diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70d4139..519f271 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,27 @@ jobs: with: node-version: "24" cache: npm + - run: npm install -g npm@latest - run: npm ci - run: npm run build - run: npm test - run: npm run check - - run: npm pack --dry-run + - name: Generate exact candidate evidence + shell: bash + run: | + mkdir -p release-evidence + npm pack --json > release-evidence/pack.json + node scripts/release-integrity.ts pack-output release-evidence/pack.json package.json + node scripts/release-integrity.ts candidate-provenance release-evidence/pack.json package.json > release-evidence/provenance.intoto.json + npm sbom --sbom-format cyclonedx > release-evidence/sbom.cdx.json + npm audit --json > release-evidence/audit.json + npm audit signatures > release-evidence/signatures.txt + node scripts/release-integrity.ts licenses package-lock.json docs/license-dispositions.json > release-evidence/licenses.json + sha256sum release-evidence/* opencode-artifacts-*.tgz + - uses: actions/upload-artifact@v4 + with: + name: exact-candidate-evidence + path: | + release-evidence/ + opencode-artifacts-*.tgz + retention-days: 14 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index dcf1e56..786de50 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,6 +23,55 @@ jobs: - run: npm run build - run: npm test - run: npm run check - - run: npm publish + - name: Verify coordinated release tag + run: node scripts/release-integrity.ts tag-version package.json "$GITHUB_REF_NAME" + - name: Pack exact release bytes + id: pack + shell: bash + run: | + mkdir -p release-evidence + npm pack --json > release-evidence/pack.json + node scripts/release-integrity.ts pack-output release-evidence/pack.json package.json >> "$GITHUB_OUTPUT" + - name: Generate and gate supply-chain evidence + shell: bash + run: | + set +e + npm sbom --sbom-format cyclonedx > release-evidence/sbom.cdx.json + sbom_status=$? + npm audit --json > release-evidence/audit.json + audit_status=$? + npm audit signatures > release-evidence/signatures.txt + signature_status=$? + node scripts/release-integrity.ts licenses package-lock.json docs/license-dispositions.json > release-evidence/licenses.json + license_status=$? + node scripts/release-integrity.ts candidate-provenance release-evidence/pack.json package.json > release-evidence/provenance.intoto.json + provenance_status=$? + set -e + sha256sum release-evidence/* "${{ steps.pack.outputs.tarball }}" + test "$sbom_status" -eq 0 + test "$audit_status" -eq 0 + test "$signature_status" -eq 0 + test "$license_status" -eq 0 + test "$provenance_status" -eq 0 + - name: Publish exact tarball + shell: bash + run: | + tar -tzf "${{ steps.pack.outputs.tarball }}" + npm publish "${{ steps.pack.outputs.tarball }}" env: NPM_CONFIG_PROVENANCE: "true" + - name: Verify published registry bytes and provenance + shell: bash + run: | + npm view "${{ steps.pack.outputs.package_spec }}" dist --json > release-evidence/registry-dist.json + node scripts/release-integrity.ts verify-registry release-evidence/pack.json package.json release-evidence/registry-dist.json + - name: Retain exact release evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: release-evidence-${{ github.ref_name }}-${{ github.run_id }} + path: | + release-evidence/ + opencode-artifacts-*.tgz + if-no-files-found: error + retention-days: 30 diff --git a/.gitignore b/.gitignore index 73b9fdc..9affde4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ +docs/evidence/journeys/raw/ dist/ *.tgz .opencode/artifacts/ diff --git a/README.md b/README.md index a24f50b..a77c9fb 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ Publish OpenCode session output as self-contained, interactive HTML artifact pages. +> **Status: public preview.** The project is publicly inspectable but currently unsupported +> and uncertified. There are zero fully supported platform/browser cells, and no representative- +> user first-use or comprehension baseline is claimed. Exact technical and supply-chain +> evidence is linked below. + [![npm](https://img.shields.io/npm/v/opencode-artifacts)](https://www.npmjs.com/package/opencode-artifacts) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) @@ -31,6 +36,7 @@ output stays diff-friendly and cheap in tokens. - [Authoring format](#authoring-format) - [Sharing and hosting](#sharing-and-hosting) - [Limitations](#limitations) +- [Governance and support](#governance-and-support) - [Development](#development) - [Product specification](#product-specification) - [Roadmap](#roadmap) @@ -47,7 +53,7 @@ output stays diff-friendly and cheap in tokens. - **Gallery + versions**: every publish updates `.opencode/artifacts/index.html`; `version: true` keeps numbered history; `restore` rolls back; a stale-version hash guard prevents blind overwrites - **Interactive**: chart-bound controls (vega-lite `params.bind`, echarts `dataZoom`), text-selection comments, workshop decision pages the session can read back - **Live reload**: `opencode-artifacts serve` refreshes open pages on every republish -- **Sharing**: cost-free public hosting via GitHub Pages, or authenticated hosting via Cloudflare Workers + KV + Access +- **Sharing**: cost-free public snapshots via GitHub Pages or a user-operated Cloudflare Worker + KV; Cloudflare Access is a manual, unverified perimeter - **Safe by default**: no raw HTML passthrough, credential-pattern scan blocks accidental secret leaks, no external requests at view time ## Install @@ -56,8 +62,11 @@ output stays diff-friendly and cheap in tokens. opencode plugin opencode-artifacts ``` -Published at [npmjs.com/package/opencode-artifacts](https://www.npmjs.com/package/opencode-artifacts) -with provenance attestations (trusted publishing, see `docs/release.md`). +Published at [npmjs.com/package/opencode-artifacts](https://www.npmjs.com/package/opencode-artifacts). +The [dated provider report](docs/evidence/governance/provider-status-2026-08-16.md) verifies +provenance for the then-current registry release. The registry trusted-publisher setting and +provenance for unreleased candidate bytes remain unverified until post-publish checks; see the +[support and release policy](docs/support-policy.md). The official OpenCode plugin command installs the package and updates the project config. Alternatively, add the npm package directly to `opencode.json`; OpenCode installs npm plugin @@ -162,7 +171,7 @@ browser-verified screenshots in [`docs/evidence/patterns/`](docs/evidence/patter | Local files | (default) | `.opencode/artifacts/.html` + gallery | | Live preview | `opencode-artifacts serve` | localhost gallery, SSE live reload, comments/decisions/mini-DB persistence | | GitHub Pages | `opencode-artifacts deploy --repo you/artifacts` | public URL per artifact; git history as audit log ([live demo](https://bitgorust.github.io/artifacts/)) | -| Cloudflare | `deploy --target cloudflare --name my-artifacts` | Workers + KV hosted gallery; comments/decisions/DB work hosted; add Access for org auth — [guide](docs/hosted-cloudflare.md) | +| Cloudflare | `deploy --target cloudflare --name my-artifacts` | User-operated public-by-default Worker + KV; comments/decisions/DB work hosted; Access is a manual perimeter — [guide](docs/hosted-cloudflare.md) | ## Limitations @@ -174,6 +183,23 @@ browser-verified screenshots in [`docs/evidence/patterns/`](docs/evidence/patter verified access policy managed by this package. - Hosted pages do not yet push new revisions into already-open browsers, and hosted MCP calls do not run through each viewer's identity. +- No complete Node/OpenCode/OS/browser cell currently meets the supported-platform evidence + gate. Existing Linux host and CI observations have narrower scopes. + +## Governance and support + +Current public-preview policy is explicit about incomplete certification evidence: + +- [support and release policy](docs/support-policy.md) — Node 24 floor, zero currently + supported full matrix cells, current-minor fix window, deprecation and D-06 supply chain; +- [security model and response](docs/security.md) and [public reporting status](SECURITY.md) — + threat boundaries and response process; private vulnerability reporting is enabled and + verified; +- [data governance](docs/data-governance.md) — no default local telemetry, mode-specific + inventory, operator/controller boundaries, retention/deletion and public abuse handling. + +These documents do not turn missing provider, platform, participant, or production evidence +into a readiness claim. ## Development @@ -234,4 +260,6 @@ visual, attach a browser screenshot. Standard and high-risk behavior changes als ## License -[MIT](LICENSE) © bitgorust +[MIT](LICENSE) © bitgorust. Documentation, examples, retained screenshots, dependency +dispositions, and link-only benchmark references are covered by the +[redistribution inventory](docs/redistribution-policy.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..83b0674 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,22 @@ +# Security policy + +## Reporting status + +Private vulnerability reporting is currently **available**. GitHub private vulnerability +reporting was enabled with explicit authorization and independently read back as enabled on +2026-08-16. Report sensitive vulnerabilities through the repository's +[private advisory form](https://github.com/bitgorust/opencode-artifacts/security/advisories/new). +Do not place exploit details, secrets, private artifact content, or personal data in a public +issue. + +Non-sensitive hardening bugs may be reported through the public issue tracker with a minimal +reproduction that contains no confidential data. The private path satisfies the intake +portion of the security gate; the broader production-readiness gate still depends on its +other required evidence. + +## Supported versions + +Security-fix support covers only the newest release in the current package minor, as defined +in [`docs/support-policy.md`](docs/support-policy.md). Older minors are unsupported and should +be upgraded. The full severity, response, disclosure, rotation, and compromised-release +process is in [`docs/security.md`](docs/security.md). diff --git a/docs/claude-code-comparison.md b/docs/claude-code-comparison.md index 3623391..c2f3706 100644 --- a/docs/claude-code-comparison.md +++ b/docs/claude-code-comparison.md @@ -12,10 +12,15 @@ through the official documentation index on 2026-08-15. The [launch post](https://claude.com/blog/artifacts-in-claude-code) is historical context: its Team/Enterprise-only, no-public-sharing beta statements have been superseded by the current guide. -The official viewer screenshot is retained at `docs/references/claude-artifact-viewer.png`. +Official viewer media is referenced through the current documentation and is not retained +locally without explicit redistribution authority. The exact disposition is recorded in +[`docs/redistribution-policy.md`](redistribution-policy.md). Binary-string research is supplemental and does not establish official behavior or parity. The target contract and honest release levels are in [`docs/product-spec.md`](product-spec.md) and [`docs/roadmap.md`](roadmap.md). +Current [support](support-policy.md), [security](security.md), and +[data-governance](data-governance.md) policies make the remaining platform, private-reporting, +authenticated-lifecycle, and supply-chain evidence gaps explicit. Local reference evidence is explicit rather than inferred: OpenCode 1.18.18 loaded this checkout and registered all four tools, while a healthy native Claude Code 2.1.233 install diff --git a/docs/component-spec.md b/docs/component-spec.md index 157d38b..7fff3bb 100644 --- a/docs/component-spec.md +++ b/docs/component-spec.md @@ -3,11 +3,12 @@ Goal: reach Claude Code Artifacts **page** quality while keeping our authoring model (model writes Markdown + JSON specs, fixed renderer owns the HTML/CSS). -Reference: `docs/references/claude-artifact-viewer.png` (official viewer screenshot), +Reference: the official [Claude Code Artifact guide](https://code.claude.com/docs/en/artifacts), `docs/claude-code-comparison.md` (pattern list from official docs), and -`docs/page-quality-benchmark.md` (comparative quality gate). +`docs/page-quality-benchmark.md` (comparative quality gate). Official media remains link-only +unless explicit redistribution authority is recorded. -## Design tokens (extracted from the official screenshot) +## Design tokens (adapted from the official visual reference) ``` --page-bg: #e9edf2 (light gray-blue) diff --git a/docs/data-governance.md b/docs/data-governance.md new file mode 100644 index 0000000..ec81657 --- /dev/null +++ b/docs/data-governance.md @@ -0,0 +1,84 @@ +# Data governance and public-sharing policy + +Policy version: 1. Last reviewed: 2026-08-16. + +The exact mode inventory is machine-readable in +[`governance-policy.json`](governance-policy.json). This is an engineering data-handling +policy, not a claim of legal compliance, residency, certification, backup, or service SLA. + +## Default and controller boundary + +Local rendering sends no project usage telemetry or analytics. No optional study or +measurement begins without affirmative informed consent, and declining does not reduce +product functionality. Explicit deployment is a capability action: fields required to push +to the user-selected GitHub or Cloudflare account cross that provider boundary and are not +quietly treated as analytics consent. + +The user or organization controlling a destination repository/account operates that target. +The project does not operate GitHub Pages or Cloudflare deployments, choose their region, +promise their backups/log retention, or convert provider terms into a project guarantee. +GitHub Pages and an unprotected Worker are public. Cloudflare Access is a manual perimeter +owned by the deployer, not verified authenticated product behavior. + +Authenticated collaboration and viewer-scoped connectors are planned. Their prospective +identity, role, session, grant, connector-result, cache, and audit fields are not current +collection. + +## Current mode inventory + +| Mode | Data and location | Retention and deletion | +|---|---|---| +| Portable local | Authored source/metadata, HTML, manifest/versions and browser localStorage on user-controlled filesystem/profile | User deletes files and browser data; user backups follow their policy | +| Loopback service | Pages, decisions, comments, mini-DB, registered datasource output in local process/project/browser | Stop process; delete `.state`, `.db`, datasource config, artifacts and browser data separately | +| GitHub Pages | Public pages/gallery, manifest, commits and repository metadata in GitHub/CDN | Operator removes content/history/repository; forks, clones, caches and backups may remain | +| Cloudflare Worker | Public pages/Worker config plus KV decisions/comments/mini-DB in user account/edge | Operator deletes Worker, KV, routes and available logs; provider cache/backup expiry is not claimed | +| Journey study | Pseudonymous consent/eligibility/platform/timing and synthetic-fixture answers in restricted raw storage | Withdrawal deletes covered raw record; all raw data deleted 30 days after aggregate acceptance | +| Release evidence | Public technical versions, digests, test results and approved fixtures/screenshots | Retained as decision history; private/unauthorized material is removed with a redacted correction | + +## List, export, correction, and deletion + +Local artifacts can be listed through the gallery/manifest and copied as files. Correction +creates or republishes a revision; immutable history is not silently rewritten. Delete the +artifact, version files, manifest entry/state and browser data according to scope. There is no +single current command that proves all local copies, backups, or browser storage are gone, so +complete erasure is unavailable. + +For GitHub Pages, repository history/export and deletion are GitHub/operator operations. For +Cloudflare, source artifacts can be copied locally; Worker/KV list/export/delete is performed +by the account operator with provider tooling. The product does not currently return a +bounded provider-wide deletion receipt or backup-expiry time. Public URLs, git history, +forks, clones, CDN caches, search indexes, screenshots, and third-party copies can outlive +removal. Authenticated/connector list, export, correction, and deletion are not implemented. + +## Evidence minimization + +Logs, metrics, traces, support bundles, screenshots, fixtures, benchmarks, studies and release +records must contain only purpose-required fields, use pseudonymous identifiers, and name an +access/retention review trigger. Credentials, identity headers, private security reports, +private artifacts and unnecessary personal data must not enter portable pages, deployment +trees, browser configuration, diagnostics, exports, or public evidence. Redact or reject them +before they leave their boundary. The journey-specific consent and withdrawal contract is in +[`journeys/README.md`](journeys/README.md). + +Current CLI and plugin publication scans authored content, frontmatter metadata, and title +overrides. GitHub Pages and Cloudflare deployment adapters rescan every current top-level +public file, provider target configuration, and reused clone/staging tree before provider +mutation. The explicit force override is scoped to one invocation and is not remembered. + +## Public abuse, takedown, and intellectual property + +The complete repository disposition is in the machine-checked +[`redistribution inventory`](redistribution-policy.md). + +Before public deployment, the operator must confirm the material is intended for a public +audience, references are unambiguous, and source/assets/fonts/examples have redistribution +authority and required attribution. Private, unlicensed, ambiguously licensed, or secret +material must not be deployed. + +Reports about a user-operated target should go first to the repository/account operator and +then through the relevant GitHub or Cloudflare abuse/IP process. A non-sensitive project bug +in the deployment adapter may use the public project issue tracker. The project can guide or +fix its software but cannot unilaterally remove a user's target or guarantee immediate global +removal. Operators should preserve the minimum record needed to evaluate a report, remove +validated material within their authority, avoid republishing it in the takedown record, and +state which history/caches/third-party copies may remain. diff --git a/docs/engineering-principles.md b/docs/engineering-principles.md index 8e51949..537f01f 100644 --- a/docs/engineering-principles.md +++ b/docs/engineering-principles.md @@ -193,6 +193,9 @@ collectively exhausted by [`docs/product-spec.md`](product-spec.md), not repeate local `npm run check` uses the same structural assertions. [check:file-ci] - Failed, excluded, flaky, unsupported, and not-applicable outcomes are visible beside passes. Evidence is never cherry-picked to protect a claim. +- Contract Markdown links to repository files and headings are validated deterministically; + authoritative external-source checks are bounded, dated, and report skipped or failed + network results instead of treating configuration as evidence. [check:docs-link-integrity] ## 9. Distribution and supply-chain integrity @@ -205,7 +208,14 @@ collectively exhausted by [`docs/product-spec.md`](product-spec.md), not repeate tarball. [check:pkg-metadata] [check:pkg-files-skills] [check:file-license] - Releases are built and tested from the exact packed bytes in clean supported hosts. They produce dependency/license/vulnerability evidence, SBOM, and provenance suitable for - consumer integrity verification. + consumer integrity verification. Platform, provider, privacy, and provenance claims may + pass only from exact dated evidence; absent or separately scoped observations stay + unverified. [check:governance-policy] +- Public preview is a non-certified distribution state, not a weaker certification level. It + may expose missing human, platform, parity, and production evidence only while claiming zero + support and passing every exact security, privacy, package, trusted-publishing, integrity, + signature, and provenance hard gate. Synthetic agents remain diagnostics; preview history + never supplies a certification waiver. - Dependencies default to zero additions. A dependency must justify capability, browser weight, CSP/network behavior, license, vulnerability surface, update ownership, and removal path. Conditional inlining is the exception that permits existing heavy visual runtimes. diff --git a/docs/evidence/contract/official-links-2026-08-16.md b/docs/evidence/contract/official-links-2026-08-16.md new file mode 100644 index 0000000..c7a7f47 --- /dev/null +++ b/docs/evidence/contract/official-links-2026-08-16.md @@ -0,0 +1,45 @@ +# Official-source link validation — 2026-08-16 + +- Result: pass +- Command: `npm run check:links -- --external` +- Execution boundary: repository working tree on `agent/goal-1-contract`, based on approval + checkpoint `116d83f` +- Local link result: 0 issues +- Official-source result: 27 unique URLs checked, 0 failures +- Timeout per request: 10 seconds +- Redirect policy: followed and reported + +## Results + +| Source URL | Result | Final URL when redirected | +|---|---|---| +| `https://cdn.openai.com/business-guides-and-resources/a-practical-guide-to-building-agents.pdf` | pass | unchanged | +| `https://claude.com/blog/artifacts-in-claude-code` | pass | unchanged | +| `https://code.claude.com/docs/en/artifacts` | pass | unchanged | +| `https://code.claude.com/docs/en/changelog` | pass | unchanged | +| `https://code.claude.com/docs/en/env-vars` | pass | unchanged | +| `https://code.claude.com/docs/en/feature-availability` | pass | unchanged | +| `https://code.claude.com/docs/en/interactive-mode` | pass | unchanged | +| `https://code.claude.com/docs/en/permissions` | pass | unchanged | +| `https://code.claude.com/docs/en/settings` | pass | unchanged | +| `https://code.claude.com/docs/en/setup` | pass | unchanged | +| `https://code.claude.com/docs/en/tools-reference` | pass | unchanged | +| `https://code.claude.com/docs/en/whats-new/2026-w25` | pass | unchanged | +| `https://code.claude.com/docs/en/whats-new/2026-w29` | pass | unchanged | +| `https://code.claude.com/docs/llms.txt` | pass | unchanged | +| `https://developers.openai.com/codex/guides/agents-md` | pass | `https://learn.chatgpt.com/docs/agent-configuration/agents-md` | +| `https://learn.chatgpt.com/use-cases/follow-goals` | pass | unchanged | +| `https://opencode.ai` | pass | `https://opencode.ai/` | +| `https://opencode.ai/docs` | pass | unchanged | +| `https://opencode.ai/docs/cli/` | pass | unchanged | +| `https://opencode.ai/docs/custom-tools/` | pass | unchanged | +| `https://opencode.ai/docs/permissions/` | pass | unchanged | +| `https://opencode.ai/docs/plugins/` | pass | unchanged | +| `https://opencode.ai/docs/server/` | pass | unchanged | +| `https://opencode.ai/docs/skills` | pass | unchanged | +| `https://platform.claude.com/docs/en/api/compliance/code/artifacts` | pass | unchanged | +| `https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents` | pass | unchanged | +| `https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills` | pass | unchanged | + +The retained table records reachability only. It does not claim that linked prose is correct, +that redirects will remain stable, or that a successful request establishes product parity. diff --git a/docs/evidence/governance/dependency-remediation-proposal-2026-08-16.md b/docs/evidence/governance/dependency-remediation-proposal-2026-08-16.md new file mode 100644 index 0000000..c9bdbc5 --- /dev/null +++ b/docs/evidence/governance/dependency-remediation-proposal-2026-08-16.md @@ -0,0 +1,52 @@ +# Dependency remediation proposal — 2026-08-16 + +Status: approved by `bitgorust` at 2026-08-16T14:57:27Z; no dependency change has yet been +applied. The approval selects the three proposed exact license branches and hash bindings. + +## Vulnerability baseline + +`npm audit --json` reported eight findings on the current lockfile: seven high and one +moderate. The direct renderer versions and current registry fix targets observed on +2026-08-16 are: + +| Package | Current | Proposed fix line | +| --- | ---: | ---: | +| `echarts` | 5.6.0 | 6.1.0 | +| `vega` | 5.33.1 | 6.4.0 | +| `vega-lite` | 5.23.0 | 6.4.3 | +| `vega-embed` | 6.29.0 | 7.1.0 | + +The relevant advisory classes include raw ECharts series names reaching tooltip HTML, +Vega debug/global gadget execution, and Vega expression `setdata` execution. These are in +scope because artifact chart specifications are untrusted user input: + +- +- +- + +## License baseline + +The release license gate currently fails closed for one missing-metadata package and two +compound-license packages. Inspection of the exact installed files produced these candidate +dispositions. The maintainer approved these exact selections at the timestamp above. + +| Package and dependency path | Installed license evidence | Proposed branch | +| --- | --- | --- | +| `mermaid@11.16.1 > khroma@2.1.0` | `node_modules/khroma/license`, SHA-256 `66b333b0f66759a0b710459e03f7029abe17f4358114a128d2c972e642961b49` | MIT — approved | +| `mermaid@11.16.1 > dompurify@3.4.13` | `node_modules/dompurify/LICENSE`, SHA-256 `cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30` | Apache-2.0 — approved | +| `@opencode-ai/plugin@1.18.18 > @ai-sdk/provider@3.0.8 > json-schema@0.4.0` | `node_modules/json-schema/LICENSE`, SHA-256 `4e053c510d6f3e4724213a292c65142df68d069c40f558582bc4270914e16f77` | BSD-3-Clause — approved | + +Alternative installed branches are DOMPurify MPL-2.0 (SHA-256 +`fab3dd6bdab226f1c08630b1dd917e11fcb4ec5e1e020e2c16f83a0a13863e85`) and json-schema +AFL-2.1 in the same compound file. The proposal prefers the permissive branches but does not +convert that preference into a broader approval for future versions, hashes, or branches. + +## Required acceptance evidence + +- Fresh audit and license reports for the exact post-upgrade lockfile and packed candidate. +- Recorded human approval for the three exact license branches and hashes. +- Compatibility and adversarial-payload tests across the real renderer surface. +- CSP, offline-network, package-removal, browser-smoke, accessibility, and size-delta results. +- SBOM, signature, provenance, and package-integrity evidence for the same candidate bytes. + +Until all of those checks pass, the release gate remains intentionally blocked. diff --git a/docs/evidence/governance/local-environment-2026-08-16.md b/docs/evidence/governance/local-environment-2026-08-16.md new file mode 100644 index 0000000..2e8699c --- /dev/null +++ b/docs/evidence/governance/local-environment-2026-08-16.md @@ -0,0 +1,25 @@ +# Local environment observation — 2026-08-16 + +Purpose: record available evidence without promoting it to a supported platform cell. + +```text +OS: Ubuntu 24.04.4 LTS; Linux 6.8.0-106-generic x86_64 +system Node: v18.19.1 +npm-script PATH Node: v24.19.0 from an extraneous node_modules binary +npm: 9.2.0 +OpenCode: 1.18.18 +Chromium/Chrome/Firefox executables: not found on PATH +``` + +The system shell is below the Node 24 support floor, the local Node 24 binary is not a clean +lockfile install, and no target browser exists. This environment is useful for repository +diagnostics only and is not a D-01 platform pass. The prior host probe records OpenCode plugin +registration but did not record a complete exact OS/Node/browser journey. + +## Later exact-candidate checkpoint + +This file preserves the initial observation. A later approved run used a complete Node 24 +distribution, the exact CI tarball, real OpenCode, and an available cached Chromium build. +See the [Ubuntu packed-candidate observation](ubuntu-packed-observation-2026-08-16.md). That +stronger technical observation still does not promote the target cell because Firefox, +latest-two-browser, and consented first-use evidence remain absent. diff --git a/docs/evidence/governance/provider-status-2026-08-16.md b/docs/evidence/governance/provider-status-2026-08-16.md new file mode 100644 index 0000000..88f322b --- /dev/null +++ b/docs/evidence/governance/provider-status-2026-08-16.md @@ -0,0 +1,16 @@ +# Provider prerequisite status — 2026-08-16 + +Repository: `bitgorust/opencode-artifacts` + +| Prerequisite | Observed result | Consequence | +|---|---|---| +| GitHub private vulnerability reporting | After explicit user authorization, authenticated repository administrator `bitgorust` enabled the setting through the repository API; a separate read returned `private_vulnerability_reporting.enabled: true` | Pass; `SECURITY.md` directs sensitive reports to the verified private advisory path | +| npm registry provenance for 0.14.3 | Registry metadata and attestation endpoint bind the published package digest to tag `v0.14.3`, commit `58f1976b745ec488cfe6dd301a972a3eeb17e10a`, `.github/workflows/publish.yml`, and GitHub Actions run `31890844916` attempt 1 | Pass for the already-published 0.14.3 bytes only | +| npm trusted-publisher configuration | After explicit user authorization and npm owner authentication as `aaron.tsang`, `npm trust list opencode-artifacts --json` returned trusted publisher ID `e60f0a5b-665f-4d39-8300-a29a21bf07a1`, repository `bitgorust/opencode-artifacts`, workflow `publish.yml`, and permission `createPackage` | Pass for the exact `v0.14.4` publisher coordinate | +| npm registry integrity, signature, and provenance for 0.14.4 | npm published the 50,698-byte tag artifact through run `31961711046`; retained and independently downloaded tarballs are identical at SHA-256 `7a529bb0cb5cc2460be7df4183315186bf7034dc1284f496448584f2e020de1e`; registry metadata contains a package signature and SLSA provenance binding tag `v0.14.4`, commit `a5ee65a588bc659d232431f55f20f404eb20e6d4`, `publish.yml`, and run attempt 1 | Pass for public preview; the run's final verifier failed only because npm 11 returned a singleton array, retained evidence passes the corrected regression-tested verifier in `b97a9b2` | + +Both provider settings were changed or confirmed only after explicit authorization on +2026-08-16 and were then read back through their authenticated provider APIs. The npm binding +is exact and passed the pre-publish gate. The separately retrieved published bytes and +attestation passed every post-publish gate; the workflow parser failure is retained rather +than hidden. diff --git a/docs/evidence/governance/redistribution-2026-08-16.md b/docs/evidence/governance/redistribution-2026-08-16.md new file mode 100644 index 0000000..f70625f --- /dev/null +++ b/docs/evidence/governance/redistribution-2026-08-16.md @@ -0,0 +1,46 @@ +# Redistribution and attribution audit — 2026-08-16 + +Status: pass for the current repository distribution inventory. This is not legal advice and +does not authorize future third-party captures or private benchmark material. + +## Scope and result + +The approved Phase 0 governance packet requires compatible licenses and attribution for +documentation, examples, embedded assets, fonts, and benchmark references. The exact +machine-readable disposition is +[`docs/redistribution-inventory.json`](../../redistribution-inventory.json). + +- Repository-authored source, policy, documentation, examples, fixtures, skills, tests, and + generated evidence are covered by the root MIT license. +- All 21 retained binary assets are repository-generated screenshots under `docs/evidence/`. + Each entry names its synthetic/repository source, MIT disposition, contributor attribution, + and exact SHA-256. +- The repository contains zero embedded font files. Renderer CSS selects system fallback + families and copies no font bytes. +- Runtime dependency versions and terms are governed separately by the lockfile, exact + license dispositions, and retained renderer-remediation evidence. +- The official Anthropic guide and launch demonstration are link-only benchmark references. + No external benchmark media is retained. + +The previous `docs/references/claude-artifact-viewer.png` copy had SHA-256 +`5784a8cdc227f204a8b7ebf9a5e6cf4056170db17d2ea96b85f11a1aba82bdf3`. It was removed because +no explicit redistribution license was established. The benchmark and component documents now +link to official material and do not treat a local copy as project-licensed evidence. + +## Enforcement + +`scripts/governance-policy.ts` scans retained image, document, audio, video, and font +extensions and compares every file to the inventory. It rejects an unknown path, missing +entry, changed digest, missing provenance/attribution, non-MIT retained project asset, local +copy of a link-only benchmark reference, or any embedded font without a new exact disposition. + +`test/governance-policy.test.ts` exercises the complete checked-in inventory, a missing asset, +a changed digest, a copied external reference, and an undisposed font. `npm run check` executes +the same repository validation on every push. + +## Boundary + +External URLs are references rather than redistributed bytes; their availability and terms +remain owned by their operators. Future same-input Claude outputs, participant material, +private artifacts, community examples, or third-party assets require explicit capture and +redistribution authority before they may enter the repository. diff --git a/docs/evidence/governance/renderer-remediation-2026-08-16.md b/docs/evidence/governance/renderer-remediation-2026-08-16.md new file mode 100644 index 0000000..878ed14 --- /dev/null +++ b/docs/evidence/governance/renderer-remediation-2026-08-16.md @@ -0,0 +1,156 @@ +# Renderer dependency remediation — 2026-08-16 + +Status: verified implementation checkpoint. Local gates and the exact pull-request merge +candidate passed; GitHub Actions retained the packed candidate and its evidence. This is not a +package release or a supported-platform claim. + +## Approval and dependency identity + +`bitgorust` approved `supply-chain-vulnerability-remediation` at +2026-08-16T14:57:27Z. The installed and lockfile-resolved renderer family is: + +| Package | Before | Candidate | +| --- | ---: | ---: | +| `echarts` | 5.6.0 | 6.1.0 | +| `vega` | 5.33.1 | 6.4.0 | +| `vega-lite` | 5.23.0 | 6.4.3 | +| `vega-embed` | 6.29.0 | 7.1.0 | +| `vega-functions` | vulnerable line | 6.2.0 | +| `vega-interpreter` | vulnerable line | 2.3.2 | + +The candidate addresses the audited ECharts tooltip, Vega global-gadget, and Vega `setdata` +advisory classes. `npm audit --package-lock-only --json` under Node 24.19.0 and npm 12.0.2 +reported zero findings at every severity. + +## License gate + +`docs/license-dispositions.json` records the approved, exact hash-bound choices. The gate reads +installed package manifests when lockfile metadata is absent, then fails if a disposed version, +declared branch, selected branch, file path, or digest changes. + +| Package | Approved branch | Exact license-file SHA-256 | +| --- | --- | --- | +| `khroma@2.1.0` | MIT | `66b333b0f66759a0b710459e03f7029abe17f4358114a128d2c972e642961b49` | +| `dompurify@3.4.13` | Apache-2.0 | `cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30` | +| `json-schema@0.4.0` | BSD-3-Clause | `4e053c510d6f3e4724213a292c65142df68d069c40f558582bc4270914e16f77` | + +The candidate inventory contains 217 lockfile package entries. The only missing-metadata and +compound entries are exactly the three approved dispositions; `npm run check:licenses` exits 0. + +## Deterministic and adversarial verification + +- All canonical examples render through the upgraded family; the intentionally malformed + `examples/incident-report.md` block remains the sole expected inline chart error. +- ECharts' built-in HTML tooltip builder encodes attacker-controlled header, name, and value. +- Vega parses and runs through the AST interpreter without invoking the `Function` constructor. +- The application does not retain a Vega View or `VEGA_DEBUG` global and catches asynchronous + Vega embed failures into the inline error surface. +- The table-driven release model blocks the candidate when any audit, license, CSP, offline, + adversarial-payload, compatibility, or packed-byte constituent is false. + +These checks are implemented in `test/renderer-security.test.ts`, +`test/release-integrity.test.ts`, and `test/render.test.ts`. + +## Real Chromium smoke + +Temporary Playwright 1.62.1 drove an already cached Chromium 145.0.7632.6 headless shell. The +browser is an observed test tool, not a promoted support-matrix cell. The normal fixture was +`examples/patterns/tune-controls.md`; the adversarial fixture contained the cited Vega +global-gadget/`setdata` forms and an ECharts Lines-series tooltip name with an image `onerror`. + +Results: + +- two of two charts rendered in each page from `file://`; +- zero unexpected requests, console errors, page errors, or dialogs; +- CSP remained `connect-src 'none'` with no `unsafe-eval`; +- `VEGA_DEBUG` and an application Vega View global were both absent; +- alert and ECharts `onerror` sentinels remained untouched before and after forced tooltip display; +- tooltip DOM contained no raw attacker image; +- Tab/Enter toggled the theme and ArrowRight changed the Vega range control; +- at 390 CSS pixels, document width stayed 390 and chart widths stayed within the viewport. + +Screenshot: [interactive renderer smoke](renderer-upgrade-chromium-2026-08-16.png). + +## Weight, packing, and removal + +Against commit `a6f983c`, the same interactive example grew from 1,639,717 to 1,732,941 bytes: +93,224 bytes (5.7%), remaining far below the 15 MiB artifact cap. Runtime bundle totals grew +from 1,609,974 to 1,703,083 bytes. The npm 9 comparison tarball grew from 49,996 to 50,016 +bytes and retained the same 43-file package surface. + +The npm 12 exact local candidate contained 43 files, 50,141 packed bytes, and 179,462 unpacked +bytes. Its SHA-256 was +`eb926c9073efd6f90b44cd8dfd5c1fed49ad1ec695744a71259dbb3aca42c103`; pack SRI was +`sha512-njtqMVW0AEDSFO+n7mnLvYflUar1yeGUHzvP55RpqQPb9sQoVeFCbhGF6Yql26kKNT4v4ZLwebNrPS3oZ2vYAA==`. +Because gzip output can vary by npm tool version, CI binds and retains its own exact tarball +rather than assuming a local digest will match. In this run the CI tarball independently matched +the local SHA-256 exactly. + +A clean temporary install generated the interactive page from that candidate tarball. The +entire installed `node_modules` tree was then moved away. Chromium still rendered both charts +with zero requests or errors; the retained HTML SHA-256 was +`ecd54d8a7d6eee16251a4f275c297692e18abdf413572707bc1da9b201f0a9e9`. + +## Exact GitHub Actions checkpoint + +[GitHub Actions run 31956792983](https://github.com/bitgorust/opencode-artifacts/actions/runs/31956792983) +passed all build, 121-test, structural, and candidate-evidence steps. The run identifies branch +head `0bf6798ac436c858a3cc2535744753a4bd2ee7f0`; the packed pull-request merge candidate and its +provenance identify merge commit `4e5d8d8f70e756787ec3be14932ab5b775ae57cb` and workflow ref +`bitgorust/opencode-artifacts/.github/workflows/ci.yml@refs/pull/1/merge`. + +Artifact `exact-candidate-evidence` (ID `9266157283`, 91,260 compressed bytes) was retained on +2026-08-16 with expiry 2026-08-30. It contains the 50,141-byte tarball plus audit, license, +pack-coordinate, provenance, CycloneDX, and signature files. Downloaded-file SHA-256 values were: + +| Retained file | SHA-256 | +| --- | --- | +| `opencode-artifacts-0.14.3.tgz` | `eb926c9073efd6f90b44cd8dfd5c1fed49ad1ec695744a71259dbb3aca42c103` | +| `audit.json` | `1a6880655b7fe998c3f6cb838d1afedac09b478a552e3f1f38bff5a0416b74b8` | +| `licenses.json` | `4bc307c41b17ccdc6e6590f6f940939033b86cf3444617abeb2b0f356e101cea` | +| `pack.json` | `11bf62fe9f5311248352cc5b059f11fa99c9f60aa90eb5005224a47f67bbf959` | +| `provenance.intoto.json` | `50bd6362f450b6f7d3932c405e584b3254bd1afc3bac5e075840ce5205803e29` | +| `sbom.cdx.json` | `39d87520d98f073f0675a1444795fd250dc6c45a038c528795c0d916f5c73d58` | +| `signatures.txt` | `96dbc2926fd8b9c4d367ef9d0b827d3c6ad9301a3aba7b872601edb3d234df12` | + +The retained audit reports zero findings at every severity. The retained signature output +reports 212 verified registry signatures and 22 verified attestations. The provenance subject +names the retained tarball and carries its exact SHA-256, merge commit, workflow ref, run ID, +and attempt. + +## SBOM, signatures, and provenance boundary + +- CycloneDX 1.5: 211 components and 212 dependency nodes; output SHA-256 + `7d65df5d9d1626c3225ea03160d5192804b3908506c599f10dc6d86f060d2d22`. +- Registry verification: 212 package signatures and 22 attestations passed; output SHA-256 + `462af43efa2bc413d42bd0cf7981188e1201c16a0ef8082aa94a6e6422cda4b8`. +- Audit output SHA-256: + `1a6880655b7fe998c3f6cb838d1afedac09b478a552e3f1f38bff5a0416b74b8`. +- License output SHA-256: + `4bc307c41b17ccdc6e6590f6f940939033b86cf3444617abeb2b0f356e101cea`. + +CI accepts both npm pack JSON shapes used by current tooling, generates a SLSA v1 +candidate provenance statement bound to tarball SHA-256, source commit, workflow reference, +and run attempt, and uploads the statement, SBOM, audit, signature, license, pack coordinate, +and tarball together. Release CI packs before those gates, publishes that exact tarball only +after they pass, and still verifies registry signature/provenance afterward. + +No registry package or provider setting was changed. This checkpoint does not prove npm +registry provenance for an unpublished future release; that remains visibly pending. + +## Retained failed and excluded observations + +- An initial `npm test` used unsupported system Node 18 after dependency installation removed + an extraneous Node 24 binary; TypeScript test loading failed before product tests ran. Under + the supported Node 24.19.0 binary, all renderer, release-integrity, build, and structural + checks passed. The local filesystem sandbox suppressed child-process stderr in the two CLI + tests and triggered a native Node async assertion in the server test; both anomalies reproduce + unchanged at base commit `a6f983c`. The repository pre-push hook outside that constrained + sandbox and the exact GitHub run both passed all 121 tests under Node 24.19.0. +- A new Chromium download stalled without output and was stopped. The smoke used the exact + pre-existing cached browser identified above. +- Browser launch inside the filesystem sandbox failed on Linux sandbox syscalls. The approved + unsandboxed browser process then produced the recorded pass; no result was inferred from the + failed launches. +- Firefox, Safari, mobile browsers, screen readers, clean OpenCode host registration, registry + publication, and trusted-publisher configuration were not exercised here. diff --git a/docs/evidence/governance/renderer-upgrade-chromium-2026-08-16.png b/docs/evidence/governance/renderer-upgrade-chromium-2026-08-16.png new file mode 100644 index 0000000..04a603b Binary files /dev/null and b/docs/evidence/governance/renderer-upgrade-chromium-2026-08-16.png differ diff --git a/docs/evidence/governance/supply-chain-2026-08-16.md b/docs/evidence/governance/supply-chain-2026-08-16.md new file mode 100644 index 0000000..861edc3 --- /dev/null +++ b/docs/evidence/governance/supply-chain-2026-08-16.md @@ -0,0 +1,104 @@ +# Supply-chain dry-run — 2026-08-16 + +Scope: working tree candidate `opencode-artifacts@0.14.3`; this is not a published release or +registry provenance result. Results retain failures and therefore do not satisfy D-06. + +## Packed bytes + +`npm pack --pack-destination /tmp/opencode-governance-pack` produced 43 files: + +```text +filename: opencode-artifacts-0.14.3.tgz +size: 49,914 bytes +sha256: 9b25247bed40bf6612326b66394544d9445be3500cb883af00d0dad39b83deb4 +sha512 SRI: sha512-Tju71XzWiYRBEsMnk+LXa+49CkfobZVG9DtZyQ2OCYrMW2kCtcLwVPoTFMumjB9TXfr41BDcRaKOLQ8ZXzUhEA== +``` + +The tarball was generated from an uncommitted policy worktree, so the final commit identity +does not yet exist and this digest is evidence of inspection only. + +## CycloneDX + +The system npm 9.2.0 has no `sbom` command. An approved temporary npm 10.9.3 invocation ran +`npm sbom --sbom-format cyclonedx` against the exact installed tree and lockfile and produced: + +```json +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "tool": "npm 10.9.3", + "root": "opencode-artifacts@0.14.3", + "components": 221, + "dependencyNodes": 222 +} +``` + +The generated document was inspected but is not treated as a release SBOM because it is not +bound to final packed/tagged bytes or retained by release CI. + +## Vulnerability audit + +`npm audit --json` returned non-zero: 8 findings (7 high, 1 moderate, 0 critical) across 225 +dependency entries. Direct affected packages include Vega, Vega-Lite, Vega Embed, and ECharts; +reported remediations require major-version changes. Representative advisories: + +- [Vega expression XSS](https://github.com/advisories/GHSA-7f2v-3qq3-vvjf) +- [Vega `setdata` XSS](https://github.com/advisories/GHSA-m9rg-mr6g-75gm) +- [ECharts XSS](https://github.com/advisories/GHSA-fgmj-fm8m-jvvx) + +No dependency was changed because dependency additions/upgrades need separate review and the +approved packet requires failures to remain visible. Production readiness fails. + +## Registry signatures and provenance + +System npm 9.2.0 returned non-zero because `@types/markdown-it@14.1.2` referenced a signing key +whose reported expiry was 2025-01-29. The approved current-tool retry with npm 10.9.3 passed: + +```text +220 packages have verified registry signatures +22 packages have verified attestations +``` + +The current-tool result is the signature verdict; the legacy failure remains recorded as a +tool-version mismatch. npm documents the command's registry-signature and provenance scope in +its [verification guide](https://docs.npmjs.com/viewing-package-provenance/). + +`npm view opencode-artifacts@0.14.3 dist --json` and the registry attestation endpoint verified +that the already-published 0.14.3 digest has SLSA provenance from tag `v0.14.3`, commit +`58f1976b745ec488cfe6dd301a972a3eeb17e10a`, `.github/workflows/publish.yml`, and GitHub +Actions run `31890844916` attempt 1. That is a pass for those published bytes only. npm +trusted-publisher configuration was not changed during this original observation. A later +authorized package-owner session on the same date verified the exact +`bitgorust/opencode-artifacts` / `publish.yml` / `createPackage` binding, as retained in +`provider-status-2026-08-16.md`; provenance for this unreleased candidate remains pending. + +## License inventory + +The lockfile contains 225 dependency package entries. SPDX/value counts were: + +```text +MIT 121; ISC 44; BSD-3-Clause 44; Apache-2.0 6; 0BSD 3; +BSD-2-Clause 2; Python-2.0 1; Unlicense 1; +(MPL-2.0 OR Apache-2.0) 1; (AFL-2.1 OR BSD-3-Clause) 1; missing 1 +``` + +`node_modules/khroma` is the missing-license entry. `node_modules/dompurify` requires selecting +MPL-2.0 or Apache-2.0, and `node_modules/json-schema` requires selecting AFL-2.1 or BSD-3-Clause. +Until those dispositions are reviewed and recorded, the license gate fails. The repository +source is MIT; full documentation/example/asset/reference attribution still requires release- +level inspection. + +## Release automation status + +The tag workflow now runs these four gates before publication, hashes their files, packs once, +publishes that exact tarball, and compares registry integrity/shasum while requiring signature +and provenance metadata. The dependency-free verifier has deterministic pass/failure tests. +Because the current audit and license gates fail, a tag workflow would stop before packaging +or publication. No tag, registry write, or provider-setting change was performed. + +## Later remediation checkpoint + +This file preserves the pre-remediation baseline and the evidence available when it was +recorded. The approved dependency and license remediation is documented separately in +[the renderer remediation checkpoint](renderer-remediation-2026-08-16.md). The original +failure explains why remediation was required; it is not the current candidate verdict. diff --git a/docs/evidence/governance/ubuntu-packed-observation-2026-08-16.md b/docs/evidence/governance/ubuntu-packed-observation-2026-08-16.md new file mode 100644 index 0000000..d720bf2 --- /dev/null +++ b/docs/evidence/governance/ubuntu-packed-observation-2026-08-16.md @@ -0,0 +1,86 @@ +# Ubuntu exact packed-candidate observation — 2026-08-16 + +Status: tested technical observation, not a supported platform/browser cell and not an +`OUT-02` participant result. + +## Exact candidate and environment + +[GitHub Actions run 31957619885](https://github.com/bitgorust/opencode-artifacts/actions/runs/31957619885) +passed for branch head `88052dc5d7b192ccddd52d91d35fe307553647bb`. Its pull-request merge +candidate was `502d861800d67a2be178f792a70a7fdaeb84173d`. Retained artifact +`exact-candidate-evidence` (ID `9266367868`, expiry 2026-08-30) supplied the exact package: + +```text +filename: opencode-artifacts-0.14.3.tgz +size: 50,206 bytes +SHA-256: f2becdaaa12e340bd445d8e5fb2e3155a231fab3e284d57494361e05cf8a25a8 +``` + +The observation environment was: + +```text +OS: Ubuntu 24.04.4 LTS (Noble), Linux 6.8.0-106-generic, x86_64 +Node: 24.19.0 from the complete official Linux x64 distribution +npm installer: 11.17.0 +OpenCode: 1.18.18 +browser: Google Chrome for Testing 145.0.7632.6 (Playwright Chromium 1.62.1) +``` + +## Clean packed install and real host + +The tarball was installed into a new temporary prefix with no repository source path. npm +installed 207 packages. The packed plugin contained `dist/plugin.js`; `src/` was absent. + +OpenCode ran with empty temporary XDG config, data, cache, and state directories and the +installed package directory as its only inline plugin. The real server returned: + +```json +{"healthy":true,"version":"1.18.18"} +``` + +Its real `/experimental/tool/ids` response contained all four shipped tools: + +```text +artifact_publish +artifact_db +artifact_state +artifact_comments +``` + +The loopback server was then stopped. Initial launches inside the filesystem sandbox failed +with a generic `ServeError` before binding; the approved unsandboxed process produced the +successful health/tool result. No model call, provider credential, publish, or external +deployment was performed. + +## CLI render, offline reopen, and removal + +The installed package CLI rendered a new synthetic two-renderer fixture without using the +checkout: + +```text +output size: 1,732,517 bytes +output SHA-256: 9fd5cf318a9c6c718103ea873f8a8215320807e77450437586780c1a8c436c94 +``` + +The output carried the strict on-disk `connect-src 'none'` CSP and bundled both Vega and +ECharts runtimes. The entire installed package/dependency tree was moved away before opening +the generated `file://` page in Chromium. Browser results: + +- two of two charts rendered; +- the only request was the initial local HTML file; +- zero console errors, page errors, or dialogs; +- document and viewport widths both remained 1280 CSS pixels; +- keyboard Enter changed the theme from system/default to dark; +- keyboard ArrowRight changed the Vega range control from 2 to 3; and +- `VEGA_DEBUG` and the application Vega View global were both undefined. + +This verifies the exact candidate's technical install, host registration, render, offline +interaction, and post-removal portability on the observed combination. + +## Why the target cell remains unverified + +The Ubuntu target requires the latest two stable Chromium and Firefox generations plus the +complete clean first-use scope. This run exercised one Chrome-for-Testing build, no Firefox, +and no consented first-time user following only the README. It therefore cannot become a +supported cell, cannot satisfy `OUT-02`, and cannot be combined with separate observations to +claim broader browser or host support. diff --git a/docs/evidence/journeys/phase-0-baseline-status.md b/docs/evidence/journeys/phase-0-baseline-status.md new file mode 100644 index 0000000..90df60d --- /dev/null +++ b/docs/evidence/journeys/phase-0-baseline-status.md @@ -0,0 +1,17 @@ +# Phase 0 journey baseline status — 2026-08-16 + +- Corpus: version 1 under `docs/journeys/` +- Protocol: approved by `bitgorust` on 2026-08-16 +- Harness: implemented and verified by `test/journey-study.test.ts`; the blank record validates + and summarizes both outcomes as `incomplete` +- Consented representative primary participants: 0 +- `OUT-02`: incomplete; no eligible first-time-user clean-machine result +- `OUT-03`: incomplete; the minimum ten representative primary-user results do not exist +- Synthetic test records: not participant evidence and excluded from acceptance +- Raw participant evidence: none collected + +This is a truthful missing-evidence record, not a failed participant outcome and not a pass. +The product owner explicitly declined the representative-user study for public preview on +2026-08-16. Under the approved `public-preview-release-gate`, these results remain incomplete +and block human-usability, support, and certified-level claims, but they do not block an +unsupported, uncertified public preview whose separate hard gates pass. diff --git a/docs/evidence/releases/2026-08-16-v0.14.4-preview.md b/docs/evidence/releases/2026-08-16-v0.14.4-preview.md new file mode 100644 index 0000000..3f5c91a --- /dev/null +++ b/docs/evidence/releases/2026-08-16-v0.14.4-preview.md @@ -0,0 +1,192 @@ +# Release evidence: `v0.14.4` + +Status: Passed public preview + +Decision date: 2026-08-16 + +Distribution status: public preview + +Certified release level: none + +Release owner: `bitgorust` + +## Claim and scope + +- User outcome being released: publicly inspectable unsupported preview of the current local + artifact package, including remediated renderer dependencies and broader deployment secret + scanning. +- Preview/support/certification label shown to users: public preview; unsupported; + uncertified; zero supported platform/browser cells. +- Archived change packets included: `contract-link-integrity`, + `supply-chain-vulnerability-remediation`, `journey-corpus-and-baseline`, + `platform-security-privacy-policy`, and `public-preview-release-gate`. +- Included capability classes: existing package CLI/plugin/local renderer behavior only; no + new certified capability level. +- Explicitly unavailable capabilities: supported local artifact core, parity, authenticated + collaboration, connector-capable artifacts, and any production-readiness claim. +- Supported Node/OpenCode/OS/browser matrix: none. Node before 24 is unsupported; every target + platform remains unverified. A historical exact Ubuntu/Chromium run is diagnostic only. +- Supported schema range and migration source versions: no new schema or migration contract. +- Support and security-fix window: unsupported preview; the current package minor remains the + only documented security-fix window, without an SLA. +- Known limitations: OUT-02 and OUT-03 have zero real participants; target platform/browser, + comparative quality, accessibility, performance, recovery, and real public-provider + certification evidence is incomplete. + +## Requirements decision + +| Requirement(s) | Pass / Fail / Incomplete / N/A | Evidence | Accountable role | Notes | +|---|---|---|---|---| +| `OUT-02` | Incomplete | `docs/evidence/journeys/phase-0-baseline-status.md` | Product maintainer | No first-time human baseline; no claim made | +| `OUT-03` | Incomplete | `docs/evidence/journeys/phase-0-baseline-status.md` | Product maintainer | Zero representative participants; synthetic probes excluded | +| `OUT-04` | Pass | `scripts/release-integrity.ts`, `docs/support-policy.md` | Product maintainer | Public preview is not certification | +| `OUT-05`–`OUT-06` | Pass | `docs/journeys/README.md`, `docs/roadmap.md` | Product maintainer | No telemetry or fabricated outcome; roadmap gate is explicit | +| `SEC-01` | Pass for preview policy | `docs/threat-model.md` | Security maintainer | Residual implementation risks remain visible | +| `SEC-10` | Pass for preview intake | `SECURITY.md`, `docs/evidence/governance/provider-status-2026-08-16.md` | `bitgorust` | Private GitHub advisory intake enabled and provider-verified | +| `PRIV-01`–`PRIV-07` | Pass for Phase 0 preview policy | `docs/data-governance.md`, `docs/redistribution-inventory.json` | Privacy maintainer | Later hosted operations remain unavailable, not claimed | +| `COMPAT-01` | Incomplete | `docs/governance-policy.json`, `docs/support-policy.md` | Compatibility maintainer | Zero supported cells; permitted only for preview | +| `DIST-03` | Pass for public preview | This record and exact tag workflow | `bitgorust` | SemVer, trusted publishing, registry integrity, signature, and provenance pass | +| `DIST-04`–`DIST-05` | Pass for exact tag | run `31961711046` evidence artifact and governance reports | Release maintainer | Zero audit findings; exact SBOM, dispositions, signatures, bytes, and provenance retained | +| `DIST-06`–`DIST-07` | Pass for preview policy | `docs/support-policy.md`, `docs/redistribution-policy.md` | Release maintainer | Current inventory and policy pass | +| `QUAL-01`–`QUAL-02` | Pass | traceability, tests, structural checks, run `31961711046` | Quality maintainer | Exact tag rerun passed before publication | +| `QUAL-06` | Pass for current package threat scope | adversarial/guard/renderer tests | Security maintainer | No broader production claim | +| `QUAL-08` | Pass | README, this record, claim-consistency tests | Quality maintainer | Missing certification evidence is prominent | + +## Product and UX evidence + +- Clean-install time-to-first-artifact (`OUT-02`): incomplete; no eligible participant. +- Comprehension study participants/results (`OUT-03`): 0; incomplete. +- Create/revise/review/share/export/restore journey evidence: create/revise/review/share corpus + and harness exist; reconnect/export/archive/restore remain deferred. +- Permission, error recovery, Unicode/locale/RTL, teardown evidence: partial and not claimed. +- Telemetry/measurement consent and default-off verification: local rendering has no project + telemetry; optional study protocol requires consent. + +## Automated package evidence + +| Command / job | Environment | Result | Output/evidence path | +|---|---|---|---| +| `npm test` | GitHub Ubuntu runner, Node 24 | Pass, 130/130 for exact tag | run `31961711046` | +| `npm run build` | GitHub Ubuntu runner, Node 24 | Pass for exact tag | run `31961711046` | +| `npm run check` | GitHub Ubuntu runner, Node 24 | Pass, 35/35 for exact tag | run `31961711046` | +| `npm pack --json` | GitHub Ubuntu runner, current npm | Pass, 43 files, 50,698 bytes | retained run artifact | +| clean registry install and render | Ubuntu 24.04, Node 24.19.0 | Pass for `opencode-artifacts@0.14.4`; installed CLI rendered `funnel-analysis.md` with strict on-disk CSP | dated maintainer command output | +| packed-host oldest supported OpenCode | none supported | Incomplete | no certification claim | +| packed-host current stable OpenCode | prior diagnostic only | Incomplete for exact release | `docs/evidence/governance/ubuntu-packed-observation-2026-08-16.md` | + +Skipped certification-only inputs are intentional for public preview, not passes. No flaky or +quarantined test was accepted. + +The tag workflow's final job conclusion was failure even though its publish step succeeded. +The retained `registry-dist.json` used npm 11's singleton-array response shape, which the +verifier at the tag accepted only as an object. Follow-up commit `b97a9b2` adds that equivalent +shape with a regression test. Running the corrected verifier against the retained tag pack and +registry response passes; a separately downloaded registry tarball is byte-for-byte identical. +This tooling failure remains visible and does not alter the published tag bytes. + +## Public-preview transition + +| Pre-publish hard gate | Pass / Fail | Exact evidence | +|---|---|---| +| tests | Pass | exact tag run: 130/130 | +| build | Pass | exact tag TypeScript build exits 0 | +| structural checks | Pass | exact tag: 35/35 registered invariants | +| exact package contents/coordinate | Pass | `opencode-artifacts-0.14.4.tgz`, 50,698 bytes, 43 files, SHA-256 `7a529bb0cb5cc2460be7df4183315186bf7034dc1284f496448584f2e020de1e` | +| final secret scan and CSP | Pass | exact tag governance, guard, renderer-security tests and structural check | +| vulnerability audit | Pass | exact tag: zero findings at every severity across 217 dependencies | +| license disposition | Pass | exact tag: 217 entries; all missing/compound cases hash-disposed | +| redistribution inventory | Pass | exact tag machine-checked inventory and 43-file package review | +| private vulnerability intake | Pass | provider readback in `docs/evidence/governance/provider-status-2026-08-16.md` | +| exact npm trusted-publisher binding | Pass | authenticated `npm trust list`: `bitgorust/opencode-artifacts`, `publish.yml`, `createPackage` | + +| Post-publish hard gate | Pass / Fail | Exact evidence | +|---|---|---| +| registry integrity equals packed bytes | Pass | retained pack/registry response plus independent tarball comparison; identical SHA-256 `7a529bb0cb5cc2460be7df4183315186bf7034dc1284f496448584f2e020de1e` | +| registry package signature | Pass | registry `dist.signatures` contains the npm package signature | +| registry provenance for exact tag/commit/workflow | Pass | SLSA statement binds `v0.14.4`, commit `a5ee65a588bc659d232431f55f20f404eb20e6d4`, `publish.yml`, and run `31961711046` attempt 1 | + +Transition result: **public-preview**. + +## Browser, accessibility, and page quality + +- Desktop/mobile/color-mode/browser matrix: zero supported cells; incomplete. +- Keyboard and screen-reader results: one prior Chromium keyboard diagnostic; screen reader + and certified matrix absent. +- Accessibility scanner and manual findings: incomplete. +- Console, asset, overflow, and offline-network results: prior exact candidate diagnostic only; + no supported-browser claim. +- Screenshots and interaction traces: governance diagnostic retained; no new visual claim. +- Page-quality report: no parity/equal-or-better claim; benchmark remains incomplete. + +## Security and privacy + +- Threat-model revision and changed boundaries: public distribution/certification boundary and + complete current deployment staging/configuration scan. +- Adversarial tests (`QUAL-06`): current renderer, credential, staging, traversal, CSP, and + payload tests passed in the exact tag workflow. +- Final-content secret scan: pass through the exact tag guard/governance suite. +- Tenant/viewer/cache/origin isolation: not applicable to this local/public-preview package + claim; hosted certification is unavailable. +- Dependency, license, and vulnerability disposition: remediated and hash-bound; exact tag + rerun passed. +- Data inventory, telemetry, retention/export/deletion: Phase 0 policy published; later + provider-wide operations remain unclaimed. +- Abuse/takedown and vulnerability reporting: public abuse policy published; private GitHub + advisory intake enabled and verified. + +## Reliability, performance, scale, and cost + +Service-backed SLO, monitoring, restore, load, soak, and cost evidence is incomplete and not +claimed by this local package preview. No migration is introduced. Rollback is a forward patch +and npm deprecation because published name/version bytes cannot be recalled reliably. + +## Real-host and provider evidence + +| Target | Account/plan class | Smoke scenario | Result | Evidence path | +|---|---|---|---|---| +| OpenCode | local diagnostic | prior exact pack install/tool discovery | Tested, not supported | `docs/evidence/governance/ubuntu-packed-observation-2026-08-16.md` | +| Claude Code reference | unauthenticated local inspection | contract/reference inventory | Partial | `docs/evidence/claude-code-host-verification.md` | +| GitHub private vulnerability reporting | public repository | enable and independent readback | Pass | `docs/evidence/governance/provider-status-2026-08-16.md` | +| npm trusted publisher | package-owner authenticated | exact repository/workflow/permission readback | Pass | `docs/evidence/governance/provider-status-2026-08-16.md` | +| npm registry | public package | exact bytes, signature, and provenance | Pass | [run `31961711046`](https://github.com/bitgorust/opencode-artifacts/actions/runs/31961711046) plus retained artifact and registry attestation | +| GitHub Pages / Cloudflare / authenticated / connector | not claimed | certification-only providers | Incomplete | not promoted by preview | + +## Supply-chain outputs + +- Packed and published exact tag: `opencode-artifacts-0.14.4.tgz`, 50,698 bytes, SHA-256 + `7a529bb0cb5cc2460be7df4183315186bf7034dc1284f496448584f2e020de1e`, npm SHA-1 + `29158590af0c47661e5045c712cb35c8e4b67a50`, SRI + `sha512-EFycALkIET5z/7rkkvGw3G+xt3+WaOabPwBLZZg1nZ3sxuapYuXxdD7rnyoPbghKnEt2GvZ1XtgtmW6VbUkjwQ==`; + tag/commit/workflow/registry cross-check passes. +- CycloneDX 1.5 JSON: 211 components, retained artifact SHA-256 + `086c960ab0b70621d96ffb00555c8db380b1f849e29eef2ce4a836126c258721`. +- Trusted publisher and published SLSA provenance: pass for the exact tag/commit/workflow. +- Registry package signature: pass; both published attestations contain transparency-log entries. +- Dependency vulnerability and license disposition: exact tag reports zero vulnerabilities + across 217 dependencies; 212 registry signatures and 22 attestations verify. +- SPDX license/attribution inventory: machine-checked repository and runtime dispositions pass. +- Release notes: security dependency remediation; complete staged/configuration secret scan; + verified private vulnerability intake; explicit unsupported public-preview contract. No + migration or public API removal. + +## Decision, rollout, and support + +- Open blockers: none for public preview. Certification-only evidence remains incomplete. +- Accepted risks: no usability/support/parity certification; accepted only for explicitly + unsupported public preview by `bitgorust` on 2026-08-16, with no expiry implied. +- Rollout: completed through the exact trusted publisher and `v0.14.4`; the tag workflow + retained all artifacts, and independent recovery verification passed after the response- + shape parser failure. +- Success signals: every hard gate passes and registry provenance binds the exact tag commit. +- Abort signals: any pre-publish failure, version/tag mismatch, auth mismatch, changed bytes, + missing signature, or missing provenance. +- Post-release monitoring: immediate registry verification and one clean registry install/ + render diagnostic passed; no SLA. +- Incident/support contact: private GitHub advisory path; public preview has no support SLA. + +Final decision: **Public preview** + +Accountable release owner: `bitgorust` + +Required role reviews: release/security/product owner approval recorded in the active packet; +certification-role reviews are not claimed. diff --git a/docs/goal-runbook.md b/docs/goal-runbook.md index 71d667e..9e099f1 100644 --- a/docs/goal-runbook.md +++ b/docs/goal-runbook.md @@ -44,7 +44,7 @@ allows parallelism; it cannot claim a later level before the accumulated earlier | Goal | Durable objective | Roadmap scope | Verifiable stopping condition | |---|---|---|---| -| 1 | Truthful, executable contract | Phase 0 | Phase 0 exit gate passes with real policy and journey evidence | +| 1 | Truthful, executable public-preview contract | Phase 0 | Phase 0 public-preview gate passes with real hard-gate evidence and missing certification inputs visible | | 2 | Durable artifact lifecycle | Phase 1 | transaction, recovery, migration, and lifecycle gates pass | | 3 | Portable rendering completeness | Phase 2 correctness track | offline asset, accessibility, security, and performance gates pass | | 4 | Native packaged OpenCode lifecycle | Phase 3 | clean packed installs and permission/lifecycle host tests pass | @@ -68,11 +68,11 @@ policy approval, provider result, or manual QA outcome. | Needed by | Human/external input | If unavailable | |---|---|---| -| Goal 1 | representative-user and policy evidence required by `OUT-02`/`OUT-03`; supported-platform access | retain missing/blocked status; do not pass Phase 0 | +| Goal 1 | npm package-owner authentication and exact public-preview provider evidence | retain the provider gate as failed/unverified; do not publish preview | | Goal 2 | supported OS filesystems for lock/migration/fault tests | do not default-enable the schema on unverified platforms | | Goal 3 | supported desktop/mobile browsers, keyboard and screen-reader QA | retain the affected compatibility/accessibility failure | | Goal 4 | oldest-supported and current stable OpenCode hosts; release-policy decisions | narrow the support claim or pause certification | -| Goal 5 | authorized current Claude Artifact runs, retention permission, and reviewers required by the quality benchmark | ship absolute improvements only; keep equal-or-better and local-core completion unverified | +| Goal 5 | representative-user evidence required by `OUT-02`/`OUT-03`, supported-platform access, authorized current Claude Artifact runs, retention permission, and benchmark reviewers | public preview may continue; keep support, equal-or-better, and local-core certification unverified | | Goal 6 | independent clients and secondary reviewers required by `OUT-03` and the Phase 4 gate | do not certify local collaboration | | Goal 7 | GitHub Pages and Cloudflare test sites/credentials; public abuse/privacy policy owners | fake tests may pass, but public certification pauses | | Goals 8–9 | identity/domain/provider architecture, two users, region/retention/SLO decisions, backup target | authenticated support remains unavailable | @@ -161,9 +161,11 @@ npm pack --dry-run git diff --check ``` -User-visible work also requires real browser/accessibility evidence; adapters need fake-runner -and authorized real-provider smoke evidence; release claims need a completed copy of -`docs/release-evidence-template.md` under `docs/evidence/releases/`. +Certified user-visible work also requires real browser/accessibility evidence; adapters need +fake-runner and authorized real-provider smoke evidence. Public preview may retain those +certification inputs as visibly incomplete but still requires its exact technical hard gates. +Every distribution needs a completed copy of `docs/release-evidence-template.md` under +`docs/evidence/releases/`. ### 8. Reconcile truth @@ -183,30 +185,36 @@ and authorized real-provider smoke evidence; release claims need a completed cop `/goal pause`/`/goal resume` around human or external checkpoints; do not start the next goal while the current one is merely “mostly done.” -## Goal 1 — Truthful, executable contract +## Goal 1 — Truthful, executable public-preview contract ### Copy-ready objective ```text /goal Execute Goal 1 in docs/goal-runbook.md: finish roadmap Phase 0 as a truthful, -executable contract. Follow the common run loop and spec-anchored workflow, retain real -journey and policy evidence, and stop only when the canonical Phase 0 gate linked below passes. -Do not fabricate participants, approvals, platform results, or evidence. Deliver on an -agent/goal-1-contract branch with a draft PR unless I authorize another delivery path. +executable public-preview contract. Follow the common run loop and spec-anchored workflow, +retain exact policy, security, package, provider, and release evidence, and stop only when the +canonical Phase 0 public-preview gate linked below passes. Keep uncollected representative- +user and full target-platform evidence visibly incomplete; do not fabricate participants, +approvals, platform results, or evidence. Deliver on an agent/goal-1-contract branch with a +draft PR unless I authorize another delivery path. ``` ### Packets, work, and gate - `contract-link-integrity` — standard; official-source and internal-anchor validation. -- `journey-corpus-and-baseline` — standard; fixtures, consent protocol, OUT-02/OUT-03 study, - and retained results. +- `journey-corpus-and-baseline` — standard; fixtures, consent protocol, strict diagnostic + harness, and honest incomplete OUT-02/OUT-03 certification status. - `platform-security-privacy-policy` — high-risk; D-01/D-06, support matrix, threat model, data inventory, telemetry, retention/deletion/abuse, vulnerability, support, and release policies. +- `public-preview-release-gate` — high-risk; non-certified preview state machine, exact hard + gates, claim consistency, provider verification, and first preview decision. Canonical scope and stopping condition: [Roadmap Phase 0](roadmap.md#phase-0--make-the-contract-truthful) -and its linked product/traceability records. Execute every unchecked canonical item; do not -copy its acceptance details here. Hand off the resolved support/schema/recovery constraints -to Goal 2 and the fixture protocol to Goals 3 and 5. +and its linked product/traceability records. The goal passes when the unsupported public- +preview transition and exact pre/post-publish gates pass with certification evidence still +truthfully incomplete; it does not certify local artifact core. Hand off schema/recovery +constraints to Goal 2 and the fixture, participant, platform, and comparative protocols to +Goals 3 and 5. ## Goal 2 — Durable artifact lifecycle @@ -306,13 +314,15 @@ retain the blocker. Deliver on an agent/goal-5-local-core branch with a draft PR hierarchy, visual scale, mobile recomposition, and browser evidence. - `blinded-page-quality-benchmark` — standard; authorized Claude/OpenCode runs, reviewer blinding, complete distributions, iteration, and report. +- `first-use-comprehension-certification` — standard; real OUT-02/OUT-03 representative-user + study and exact claimed support-cell coverage. - `local-artifact-core-release` — high-risk; exact-package, support, migration, security, privacy, performance, supply-chain, and release-decision evidence. Canonical scope and stopping condition: [Roadmap Phase 2](roadmap.md#phase-2--portable-page-completeness), the benchmark's [hard gates](page-quality-benchmark.md#hard-gates) and [equal-or-better threshold](page-quality-benchmark.md#equal-or-better-threshold), and the -[recurring release gate](roadmap.md#recurring-release-gate--prove-and-support-the-claimed-level). +[recurring certification gate](roadmap.md#recurring-certification-gate--prove-and-support-the-claimed-level). Goal 6 starts from the resulting certified baseline. ## Goal 6 — Reliable local collaboration @@ -337,7 +347,7 @@ authorize otherwise. - `local-collaboration-release` — accumulated release/evidence packet. Canonical scope and stopping condition: [Roadmap Phase 4](roadmap.md#phase-4--reliable-local-collaboration) -and the [recurring release gate](roadmap.md#recurring-release-gate--prove-and-support-the-claimed-level). +and the [recurring certification gate](roadmap.md#recurring-certification-gate--prove-and-support-the-claimed-level). Freeze the verified shared collaboration semantics for authenticated hosting. ## Goal 7 — Honest public snapshots @@ -363,7 +373,7 @@ site without explicit scoped authority. - `public-snapshot-policy-release` — privacy/operator/retention/abuse/cost docs and release evidence. Canonical scope and stopping condition: [Roadmap Phase 5A](roadmap.md#5a-public-snapshot-adapters) -and the [recurring release gate](roadmap.md#recurring-release-gate--prove-and-support-the-claimed-level). +and the [recurring certification gate](roadmap.md#recurring-certification-gate--prove-and-support-the-claimed-level). Retain provider/teardown evidence and resolve the linked authenticated-hosting decisions before Goal 8. @@ -418,7 +428,7 @@ agent/goal-9-auth-collab branch with a draft PR unless I authorize otherwise. origin-bypass evidence and release decision. Canonical scope and stopping condition: [Roadmap Phase 5B](roadmap.md#5b-authenticated-reference-deployment) -and the [recurring release gate](roadmap.md#recurring-release-gate--prove-and-support-the-claimed-level). +and the [recurring certification gate](roadmap.md#recurring-certification-gate--prove-and-support-the-claimed-level). Freeze the verified viewer, policy, audit, state, event, and operations contracts for Goal 10. ## Goal 10 — Connector-capable artifacts and complete target @@ -447,7 +457,7 @@ definition linked below pass. Deliver on an agent/goal-10-connectors branch with performance/compatibility/supply-chain/quality evidence and release decision. Canonical scope and stopping condition: [Roadmap Phase 6](roadmap.md#phase-6--viewer-scoped-connectors-and-governance), -the [recurring release gate](roadmap.md#recurring-release-gate--prove-and-support-the-claimed-level), +the [recurring certification gate](roadmap.md#recurring-certification-gate--prove-and-support-the-claimed-level), and the product [definition of complete](product-spec.md#14-definition-of-complete). Missing external evidence produces a blocker or reduced claim, never “done.” diff --git a/docs/governance-policy.json b/docs/governance-policy.json new file mode 100644 index 0000000..34da67e --- /dev/null +++ b/docs/governance-policy.json @@ -0,0 +1,239 @@ +{ + "schemaVersion": 1, + "policyVersion": 1, + "reviewedAt": "2026-08-16", + "owners": { + "security": "Security maintainer", + "privacy": "Privacy maintainer", + "compatibility": "Compatibility maintainer", + "release": "Release maintainer" + }, + "providerPrerequisites": [ + { + "id": "github-private-vulnerability-reporting", + "status": "pass", + "checkedAt": "2026-08-16", + "evidence": "docs/evidence/governance/provider-status-2026-08-16.md", + "claim": "Private vulnerability reporting is enabled and verified; sensitive reports use the repository's private advisory path." + }, + { + "id": "npm-trusted-publishing", + "status": "pass", + "checkedAt": "2026-08-16", + "evidence": "docs/evidence/governance/provider-status-2026-08-16.md", + "claim": "The exact GitHub repository/workflow trusted publisher is provider-verified for createPackage; published 0.14.4 registry bytes, package signature, and SLSA provenance match tag v0.14.4, commit a5ee65a, publish.yml, and run 31961711046." + } + ], + "supportCells": [ + { + "id": "observed-linux-opencode", + "class": "tested", + "status": "tested", + "os": "Ubuntu 24.04.4 LTS, Linux 6.8.0-106-generic, x86_64", + "node": "24.19.0", + "opencode": "1.18.18", + "browser": "Chrome for Testing 145.0.7632.6 only; Firefox and latest-two coverage absent", + "scope": "Exact CI tarball install, real plugin tool discovery, CLI render, offline file interaction, and package-tree removal; human first-use absent", + "testedAt": "2026-08-16", + "evidence": "docs/evidence/governance/ubuntu-packed-observation-2026-08-16.md" + }, + { + "id": "ubuntu-lts-desktop", + "class": "target", + "status": "unverified", + "os": "Ubuntu 24.04 LTS", + "node": "24.x", + "opencode": "current stable 1.x; oldest-supported equals the same tested version initially", + "browser": "latest two stable Chromium and Firefox generations", + "scope": "Clean packed install, create/reopen, plugin tools, offline pages, and browser interaction", + "testedAt": null, + "evidence": null + }, + { + "id": "macos-current-desktop", + "class": "target", + "status": "unverified", + "os": "current macOS", + "node": "24.x", + "opencode": "current and oldest-supported stable 1.x", + "browser": "latest two stable Safari, Chromium, and Firefox generations", + "scope": "Clean packed install, create/reopen, plugin tools, offline pages, and browser interaction", + "testedAt": null, + "evidence": null + }, + { + "id": "macos-previous-desktop", + "class": "target", + "status": "unverified", + "os": "previous macOS", + "node": "24.x", + "opencode": "current and oldest-supported stable 1.x", + "browser": "latest two stable Safari, Chromium, and Firefox generations", + "scope": "Clean packed install, create/reopen, plugin tools, offline pages, and browser interaction", + "testedAt": null, + "evidence": null + }, + { + "id": "windows-11-native-desktop", + "class": "target", + "status": "unverified", + "os": "Windows 11 native", + "node": "24.x", + "opencode": "current and oldest-supported stable 1.x", + "browser": "latest two stable Chromium and Firefox generations", + "scope": "Clean packed install, create/reopen, plugin tools, offline pages, and browser interaction", + "testedAt": null, + "evidence": null + }, + { + "id": "windows-11-wsl-desktop", + "class": "target", + "status": "unverified", + "os": "Windows 11 with WSL 2 and Ubuntu 24.04", + "node": "24.x", + "opencode": "current and oldest-supported stable 1.x", + "browser": "latest two stable Windows Chromium and Firefox generations opening WSL-created files", + "scope": "Clean packed install, create/reopen, plugin tools, offline pages, and browser interaction", + "testedAt": null, + "evidence": null + }, + { + "id": "android-chrome-viewer", + "class": "target", + "status": "unverified", + "os": "current supported Android", + "node": "not applicable (viewer only)", + "opencode": "not applicable (viewer only)", + "browser": "latest two stable Android Chrome generations", + "scope": "Open, read, navigate, and interact with a deployed portable page", + "testedAt": null, + "evidence": null + }, + { + "id": "ios-safari-viewer", + "class": "target", + "status": "unverified", + "os": "current and previous supported iOS", + "node": "not applicable (viewer only)", + "opencode": "not applicable (viewer only)", + "browser": "latest two stable iOS Safari generations", + "scope": "Open, read, navigate, and interact with a deployed portable page", + "testedAt": null, + "evidence": null + }, + { + "id": "node-before-24", + "class": "unsupported", + "status": "unsupported", + "os": "any", + "node": "before 24", + "opencode": "any", + "browser": "any", + "scope": "Package execution", + "testedAt": null, + "evidence": null + } + ], + "dataInventory": [ + { + "mode": "portable-local", + "availability": "current", + "fields": "Authored Markdown, title/description/source metadata, rendered HTML, manifest, versions, localStorage decisions/comments", + "purpose": "Create, reopen, version, and interact with a self-contained local artifact", + "controllerOperator": "User and machine owner", + "recipientLocation": "User-selected local filesystem and browser profile; no project telemetry recipient", + "sensitivity": "User-authored content; may be confidential", + "retention": "Until the user removes artifact, manifest/version files, and browser site data", + "deletion": "Delete the artifact directory and relevant browser file-origin/site data; backups remain under the user's own policy" + }, + { + "mode": "loopback-service", + "availability": "current", + "fields": "Artifact bytes, comments, decisions, mini-DB documents, registered datasource output, local request metadata", + "purpose": "Preview and local collaboration state on 127.0.0.1", + "controllerOperator": "User running the process", + "recipientLocation": "Local process, project artifact directory, and local browser", + "sensitivity": "User-authored content and potentially sensitive command output", + "retention": "Files persist until user deletion; transient request data lasts for process/request lifetime", + "deletion": "Stop the service and delete .state/.db/datasource configuration and artifact files; remove browser site data separately" + }, + { + "mode": "github-pages-public", + "availability": "current", + "fields": "Artifact/gallery bytes, manifest, git commits and repository metadata", + "purpose": "Publish a public static snapshot selected by the user", + "controllerOperator": "User or organization controlling the destination repository", + "recipientLocation": "GitHub repository, Pages CDN/caches, and public viewers under GitHub terms", + "sensitivity": "Public; secrets and private content are prohibited", + "retention": "Repository history persists until operator action; forks, clones, caches, and backups may outlive deletion", + "deletion": "Operator removes Pages content/history or repository and follows GitHub procedures; global erasure is not guaranteed" + }, + { + "mode": "cloudflare-public", + "availability": "current", + "fields": "Artifact/gallery bytes, Worker configuration, KV decisions/comments/mini-DB documents, provider request metadata", + "purpose": "Publish a user-operated Worker and retain interactive state", + "controllerOperator": "User or organization controlling the Cloudflare account", + "recipientLocation": "Selected Cloudflare account, edge network, KV, caches/logs under the account plan and provider terms", + "sensitivity": "Public by default; KV state may contain viewer-authored content", + "retention": "Until operator deletes Worker/KV data, subject to provider cache, log, and backup schedules", + "deletion": "Operator deletes Worker, KV namespace/keys, routes and provider logs where available; project cannot guarantee provider-wide erasure" + }, + { + "mode": "authenticated-hosting", + "availability": "planned", + "fields": "Planned audience policy, viewer identity/roles, sessions, comments/state, audit and administrative events", + "purpose": "Future private collaboration and revocation", + "controllerOperator": "Not selected; future deployment operator", + "recipientLocation": "Not selected", + "sensitivity": "Private identity and artifact data", + "retention": "Not implemented", + "deletion": "Not implemented; no current product claim" + }, + { + "mode": "viewer-connectors", + "availability": "planned", + "fields": "Planned viewer grants, provider identity, query parameters/results, cache and audit metadata", + "purpose": "Future viewer-scoped data access", + "controllerOperator": "Not selected; future user/deployment operator", + "recipientLocation": "Not selected provider and connector", + "sensitivity": "Potentially private third-party data and credentials", + "retention": "Not implemented", + "deletion": "Not implemented; no current product claim" + }, + { + "mode": "journey-study", + "availability": "protocol-ready", + "fields": "Pseudonymous code, categorical eligibility/conflict, consent/withdrawal, versions/platform, bounded timings, synthetic-fixture answers and scores", + "purpose": "Measure Phase 0 first-use and comprehension outcomes", + "controllerOperator": "Study maintainer named in the dated private record", + "recipientLocation": "Access-controlled maintainer storage; anonymous aggregate in repository", + "sensitivity": "Pseudonymous research record; direct identity and private artifact content prohibited", + "retention": "Raw data until 30 days after aggregate acceptance; anonymous aggregate retained as decision history", + "deletion": "Delete raw record immediately on timely withdrawal and delete all raw data at retention deadline" + }, + { + "mode": "release-evidence", + "availability": "current", + "fields": "Commit/tag/digest, tool/platform versions, test outputs, public fixtures/screenshots, pseudonymous failure summaries", + "purpose": "Verify release claims and retain decision history", + "controllerOperator": "Project release maintainer", + "recipientLocation": "Public repository and CI/provider logs, except explicitly private raw inputs", + "sensitivity": "Public technical evidence; secrets, identities, private artifacts and vulnerability details prohibited", + "retention": "Retained with release decision history; reviewed when a source or claim changes", + "deletion": "Remove unauthorized/private material promptly while preserving a redacted correction and decision record" + } + ], + "threatBoundaries": [ + { "id": "portable-page", "availability": "current", "evidence": "docs/threat-model.md#portable-declarative-pages" }, + { "id": "trusted-html", "availability": "current", "evidence": "docs/threat-model.md#trusted-html-mode" }, + { "id": "filesystem", "availability": "current", "evidence": "docs/threat-model.md#filesystem-and-local-state" }, + { "id": "loopback", "availability": "current", "evidence": "docs/threat-model.md#loopback-service" }, + { "id": "deployment", "availability": "current", "evidence": "docs/threat-model.md#deployment-adapters" }, + { "id": "public-static", "availability": "current", "evidence": "docs/threat-model.md#public-static-targets" }, + { "id": "hosted-content-control", "availability": "partial", "evidence": "docs/threat-model.md#hosted-content-and-control-plane" }, + { "id": "audience-identity", "availability": "planned", "evidence": "docs/threat-model.md#audience-and-identity" }, + { "id": "mutable-state", "availability": "partial", "evidence": "docs/threat-model.md#mutable-collaboration-state" }, + { "id": "connectors", "availability": "planned", "evidence": "docs/threat-model.md#viewer-scoped-connectors" } + ] +} diff --git a/docs/hosted-cloudflare.md b/docs/hosted-cloudflare.md index 5c5ef82..b46f9ba 100644 --- a/docs/hosted-cloudflare.md +++ b/docs/hosted-cloudflare.md @@ -1,13 +1,20 @@ -# Authenticated hosting on Cloudflare (free tier) +# User-operated hosting on Cloudflare -The `cloudflare` deploy target publishes your artifact gallery to a Cloudflare Worker with a +The `cloudflare` deploy target publishes your artifact gallery to a Cloudflare Worker in your +account with a KV-backed state store: workshop decisions, comments, and the mini-DB all work on the hosted site — the same API surface as local `serve`, minus shell datasources (`/__data` returns 501, since Workers can't run local commands). -Free-tier coverage: Workers (100k requests/day), KV (100k reads / 1k writes per day), -Workers Static Assets (included). Cloudflare Access is free for up to 50 users and adds -org-grade identity in front of the whole site. +The generated `workers.dev` URL is public by default. You are the deployment operator and +controller; Cloudflare is the provider under your account and terms. This project does not +select a region, operate the service, promise backups/log retention or an SLA, or verify +Cloudflare Access. See the [data governance policy](data-governance.md) before deploying. + +Cloudflare's current published Free limits include 100,000 +[Worker requests/day](https://developers.cloudflare.com/workers/platform/limits/) and 100,000 +[KV reads plus 1,000 writes/day](https://developers.cloudflare.com/kv/platform/limits/). +Plans and quotas can change; confirm them in the provider account before relying on them. ## One-time setup @@ -37,7 +44,7 @@ org-grade identity in front of the whole site. you need before redeploying an older Worker. Static artifact HTML and the local manifest format are unchanged. -## Add identity (Cloudflare Access, free ≤ 50 users) +## Add a manual identity perimeter with Cloudflare Access This is currently a manual operator step. Until Access is configured and verified, the Workers URL is public; the package does not yet provide Claude-style private-by-default @@ -47,8 +54,9 @@ sharing, roles, or revocation. See the authenticated-hosting phase in 1. Cloudflare dashboard → Zero Trust → Access → Applications → Add → Self-hosted. 2. Point it at `my-artifacts..workers.dev` (or your custom route). 3. Pick an auth method (one-time PIN email is zero-config; GitHub/Google IdP also free). -4. Now every viewer is authenticated; Access injects `Cf-Access-Authenticated-User-Email` - headers the worker could use for comment authorship (roadmap). +4. Verify the origin cannot be reached without Access. If correctly configured, Access can + inject identity headers, but this package does not consume them or authorize comment + authorship; built-in identity remains roadmap work. ## What works where diff --git a/docs/journeys/README.md b/docs/journeys/README.md new file mode 100644 index 0000000..6f38e9b --- /dev/null +++ b/docs/journeys/README.md @@ -0,0 +1,104 @@ +# Phase 0 journey corpus and study protocol + +Status: approved protocol; participant baseline not yet run + +This directory owns the versioned create/revise/review/share corpus and the Phase 0 protocols +for `OUT-02`, `OUT-03`, `OUT-05`, and `UX-01`. It does not contain participant identities or +claim a result before a consented study is complete. + +## Corpus + +`corpus.json` is the machine-readable index. `study.schema.json` defines the strict private +record shape and `records.template.json` is a blank instance. The four comprehension fixtures under +`fixtures/` contain only synthetic project content and explicit provenance. Participant-facing +instructions name the task but never reveal the scoring rubric. + +The workflow corpus covers: + +1. first-use create/reopen from a released package and only README instructions; +2. create a local artifact and identify its stable path and current capability; +3. revise by the returned reference/hash, retaining a refusal as a visible outcome; +4. review a page and bring a finding or decision back to the session; and +5. share only after selecting a target and acknowledging its actual visibility. + +Reconnect, export, archive, and restore remain later-phase or partial journeys. Their absence +is visible in `corpus.json`; Phase 0 does not claim those target workflows ship. + +## Participant criteria + +A representative primary participant is an individual developer who uses a terminal coding +agent and has not maintained or contributed to this repository. Record prior OpenCode and +artifact-tool familiarity categorically. For the `OUT-02` first-use task, the participant must +also never have installed or used `opencode-artifacts` before the timed run. + +Maintainers, contributors, people who reviewed the corpus/rubrics, assisted runs, and duplicate +participants do not enter the acceptance denominator. They remain visible as exclusions in +the private raw record and aggregate counts. + +## Consent and data handling + +Before a run, tell the participant: + +- purpose: evaluate first-use and page comprehension for Phase 0; +- collected data: a random participant code, categorical eligibility/conflict fields, declared + platform/tool versions, bounded timestamps/durations, answers about synthetic fixtures, + rubric scores, assistance/failure state, and consent/withdrawal timestamps; +- not collected: name, email, account identifier, private repository or artifact content, + telemetry, audio, video, screen recording, or unrelated behavior; +- location/access: raw JSON remains in an access-controlled maintainer study directory and is + never committed; only a non-identifying aggregate report and raw-file digest are retained; +- retention: raw records are deleted 30 days after the aggregate is accepted, or immediately + on withdrawal before that deletion date; the anonymous aggregate remains as decision history; +- participation is voluntary, declining or withdrawing does not affect product functionality. + +Record affirmative consent before timing. A declined participant produces no retained record. +The template and synthetic tests are never participant evidence. + +## Prepare the exact pages + +From a clean supported machine, install the exact release candidate named in the study record. +Render each assigned source without modifying it: + +```text +opencode-artifacts render docs/journeys/fixtures/incident.md -o /tmp/journey-incident.html +``` + +Record the SHA-256 of the resulting HTML as `artifactSha256`. A participant receives one +fixture, balanced across the corpus as evenly as recruitment permits. + +## OUT-02 first-use protocol + +1. Confirm the machine is clean for the package and no repository checkout is available. +2. Start timing immediately before showing the README. +3. The participant may use only README instructions. Do not coach or diagnose during timing. +4. Success requires installing the exact released package, creating one offline artifact, and + reopening that file without a hosting account in at most 600 seconds. +5. Stop at success, ten minutes, assistance, repository use, or an unrecoverable failure. +6. Record every result, including the first failure step. Each platform claimed supported in + the study header needs at least one eligible passing first-use record. + +## OUT-03 comprehension protocol + +1. Open the assigned rendered fixture and start timing once useful content is visible. +2. Ask, without examples or coaching: “What is this page for? What is its primary finding or + current state? Where did its information come from? What should happen next?” +3. Stop at 60 seconds. Record the exact concise answers about the synthetic fixture. +4. Score each field against the fixture rubric in `corpus.json`. All four fields, no assistance, + and no more than 60 seconds are required for a participant pass. +5. At least ten eligible representative primary participants are required. At least 90% must + pass; do not round, impute missing answers, or remove failures. + +## Validate and summarize + +Keep raw files under `docs/evidence/journeys/raw/` (gitignored): + +```text +npm run study -- validate docs/evidence/journeys/raw/phase-0.json +npm run study -- summarize docs/evidence/journeys/raw/phase-0.json +``` + +Validation rejects direct-identity fields, absent consent, unknown fixtures, malformed hashes, +duplicate participants, inconsistent timing, and incomplete answers/scores. Summary output +contains no participant IDs or answer text. Retain the command output, raw-file SHA-256, +corpus/release identifiers, all failure/exclusion counts, and the access-controlled raw-evidence +owner in the dated aggregate report. diff --git a/docs/journeys/corpus.json b/docs/journeys/corpus.json new file mode 100644 index 0000000..28c0e3a --- /dev/null +++ b/docs/journeys/corpus.json @@ -0,0 +1,108 @@ +{ + "schemaVersion": 1, + "corpusVersion": 1, + "journeys": [ + { + "id": "first-use-create-reopen", + "stage": "create", + "status": "protocol-ready", + "purpose": "Install the released package, create one offline artifact, and reopen it from README instructions.", + "preconditions": ["eligible first-time user", "clean claimed platform", "exact released package", "no repository checkout", "no hosting account"], + "success": "Offline artifact is created and reopened in at most 600 seconds without assistance.", + "failure": "Retain timeout, assistance, checkout/hosting use, or first failed step.", + "decisionState": ["artifact reference", "local visibility", "portable capability"] + }, + { + "id": "create-local", + "stage": "create", + "status": "shipped-partial", + "purpose": "Publish a Markdown artifact locally and inspect the returned path/hash.", + "preconditions": ["package installed", "local write permission"], + "success": "Tool returns stable path and content hash; page opens offline.", + "failure": "Permission, validation, secret, or size refusal remains visible.", + "decisionState": ["slug/path reference", "content hash", "local visibility", "portable capability"] + }, + { + "id": "revise-by-reference", + "stage": "revise", + "status": "shipped-partial", + "purpose": "Republish using the exact artifact reference and expected content hash.", + "preconditions": ["existing local artifact", "last-read content hash"], + "success": "Stable path is refreshed and the new hash is returned.", + "failure": "A stale hash refuses without writing and returns bounded live content.", + "decisionState": ["path reference", "head hash", "local visibility", "portable capability"] + }, + { + "id": "review-bring-back", + "stage": "review", + "status": "shipped-partial", + "purpose": "Inspect a portable page and copy or record a result for the OpenCode session.", + "preconditions": ["rendered fixture", "portable or served capability stated"], + "success": "Viewer identifies or records a result and the session can consume the supported output.", + "failure": "Unavailable served-only state remains visible and is not presented as portable persistence.", + "decisionState": ["artifact reference", "current content", "local/served capability"] + }, + { + "id": "share-explicit-target", + "stage": "share", + "status": "shipped-partial", + "purpose": "Select a public snapshot target separately from local rendering.", + "preconditions": ["existing artifact", "named provider target", "deploy permission"], + "success": "User confirms actual public visibility and receives the target URL.", + "failure": "Denied, failed, or unverified deployment leaves the local artifact unchanged.", + "decisionState": ["artifact reference", "target", "public visibility", "public-static capability"] + }, + { + "id": "later-lifecycle", + "stage": "reconnect-export-archive-restore", + "status": "deferred", + "purpose": "Track later lifecycle journeys without claiming Phase 0 delivery.", + "preconditions": ["Phase 1 lifecycle contract"], + "success": "Defined and tested by the owning later phase.", + "failure": "Current absence remains explicit.", + "decisionState": ["stable artifact ID", "revision", "archive/restore capability"] + } + ], + "fixtures": [ + { + "id": "incident", + "source": "docs/journeys/fixtures/incident.md", + "rubric": { + "purpose": "Summarize and respond to checkout incident 4172.", + "primaryFinding": "A synchronous fraud check in svc-payments 1.88.0 caused the latency spike.", + "provenance": "Incident timeline, payment deploy log, latency injection, and ledger checks captured 2026-08-15.", + "nextAction": "Move the fraud check async with a 200 ms budget and default-allow timeout under PAY-2210." + } + }, + { + "id": "release", + "source": "docs/journeys/fixtures/release.md", + "rubric": { + "purpose": "Show release 2.14 readiness.", + "primaryFinding": "The performance baseline is the remaining hard blocker.", + "provenance": "Release tracker snapshot and staging migration run captured 2026-08-15.", + "nextAction": "Restore the benchmark cluster and rerun the performance baseline." + } + }, + { + "id": "review", + "source": "docs/journeys/fixtures/review.md", + "rubric": { + "purpose": "Review PR 412's cache TTL change.", + "primaryFinding": "Dropping the default TTL from 300 seconds to 5 seconds would hammer the origin.", + "provenance": "PR 412 diff and Wednesday-Thursday deploy failure investigation captured 2026-08-15.", + "nextAction": "Restore the 300-second default and reject TTL values below one before merge." + } + }, + { + "id": "migration", + "source": "docs/journeys/fixtures/migration.md", + "rubric": { + "purpose": "Plan migration of session storage from Redis to Postgres.", + "primaryFinding": "The plan is four of six complete, with dual-write drift and unverified indexes as open risks.", + "provenance": "Session architecture notes and 30-day volume assumptions captured 2026-08-15.", + "nextAction": "Decide anonymous-session retention and benchmark indexes before Phase 2 sign-off." + } + } + ] +} diff --git a/docs/journeys/fixtures/incident.md b/docs/journeys/fixtures/incident.md new file mode 100644 index 0000000..4640e04 --- /dev/null +++ b/docs/journeys/fixtures/incident.md @@ -0,0 +1,16 @@ +--- +title: Incident 4172 — Checkout latency spike +icon: 🚨 +source: Incident timeline, payment deploy log, latency injection, and ledger checks captured 2026-08-15 +--- + +Checkout p99 peaked at 2.6 seconds and timeouts reached 2.1% for 38 minutes. + +## Primary finding + +The `svc-payments@1.88.0` deploy added a synchronous fraud check to the hot path. Injecting +800 ms of fraud-service latency reproduced the spike; rolling back to 1.87.2 restored p99. + +```callout +{ "tone": "info", "title": "Next action", "body": "Implement PAY-2210: run the fraud check asynchronously with a 200 ms budget and default-allow on timeout." } +``` diff --git a/docs/journeys/fixtures/migration.md b/docs/journeys/fixtures/migration.md new file mode 100644 index 0000000..64f3962 --- /dev/null +++ b/docs/journeys/fixtures/migration.md @@ -0,0 +1,19 @@ +--- +title: Migration plan — sessions to Postgres +icon: 🗺️ +source: Session architecture notes and 30-day volume assumptions captured 2026-08-15 +--- + +```progress +{ "label": "Plan confidence", "done": 4, "total": 6 } +``` + +## Current state + +The three-deploy dual-write/backfill/cutover approach is defined. Open risks are silent +dual-write drift and index choices that have not been benchmarked at 30-day volume. + +## Next action + +Decide retention for anonymous sessions and benchmark the proposed indexes before Phase 2 +sign-off. diff --git a/docs/journeys/fixtures/release.md b/docs/journeys/fixtures/release.md new file mode 100644 index 0000000..f54d80f --- /dev/null +++ b/docs/journeys/fixtures/release.md @@ -0,0 +1,18 @@ +--- +title: Release 2.14 readiness +icon: ✅ +source: Release tracker snapshot and staging migration run captured 2026-08-15 +--- + +```progress +{ "label": "Release readiness", "done": 7, "total": 11 } +``` + +## Current state + +Migration 0412 passed its staging dry run and the rollback plan is reviewed. Security sign-off +is pending, but the only hard blocker is the performance baseline because the benchmark +cluster is down. + +> [!IMPORTANT] +> Restore the benchmark cluster and rerun the performance baseline before tagging. diff --git a/docs/journeys/fixtures/review.md b/docs/journeys/fixtures/review.md new file mode 100644 index 0000000..c2f903d --- /dev/null +++ b/docs/journeys/fixtures/review.md @@ -0,0 +1,14 @@ +--- +title: PR 412 review — cache TTL configuration +icon: 🔀 +source: PR 412 diff and Wednesday–Thursday deploy-failure investigation captured 2026-08-15 +--- + +## Primary finding + +The PR drops the default cache TTL from 300 seconds to 5 seconds, which would increase origin +requests roughly sixtyfold. It also leaves validation disabled, so zero or negative TTLs pass. + +```callout +{ "tone": "warn", "title": "Merge decision", "body": "Restore the 300-second default and reject ttlSeconds below one before merge." } +``` diff --git a/docs/journeys/records.template.json b/docs/journeys/records.template.json new file mode 100644 index 0000000..9d7524a --- /dev/null +++ b/docs/journeys/records.template.json @@ -0,0 +1,12 @@ +{ + "schemaVersion": 1, + "studyId": "replace-with-dated-study-id", + "corpusVersion": 1, + "release": { + "package": "opencode-artifacts", + "version": "replace-with-exact-version", + "integrity": "sha512-replace-with-registry-or-tarball-integrity" + }, + "claimedPlatformIds": [], + "records": [] +} diff --git a/docs/journeys/study.schema.json b/docs/journeys/study.schema.json new file mode 100644 index 0000000..556f51d --- /dev/null +++ b/docs/journeys/study.schema.json @@ -0,0 +1,133 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/bitgorust/opencode-artifacts/blob/main/docs/journeys/study.schema.json", + "title": "Phase 0 journey study records", + "type": "object", + "additionalProperties": false, + "required": ["schemaVersion", "studyId", "corpusVersion", "release", "claimedPlatformIds", "records"], + "properties": { + "schemaVersion": { "const": 1 }, + "studyId": { "type": "string", "minLength": 1, "maxLength": 120 }, + "corpusVersion": { "const": 1 }, + "release": { + "type": "object", + "additionalProperties": false, + "required": ["package", "version", "integrity"], + "properties": { + "package": { "const": "opencode-artifacts" }, + "version": { "type": "string", "minLength": 1, "maxLength": 80 }, + "integrity": { "type": "string", "pattern": "^(sha512-|sha256:)[A-Za-z0-9+/=:.-]+$" } + } + }, + "claimedPlatformIds": { + "type": "array", + "uniqueItems": true, + "items": { "type": "string", "minLength": 1 } + }, + "records": { + "type": "array", + "maxItems": 100, + "items": { "$ref": "#/$defs/record" } + } + }, + "$defs": { + "record": { + "type": "object", + "additionalProperties": false, + "required": ["participantId", "participantRole", "representative", "firstTimeUser", "conflict", "synthetic", "consent", "platform", "firstUse", "comprehension"], + "properties": { + "participantId": { "type": "string", "pattern": "^(p-[a-z0-9]{6,}|synthetic-[a-z0-9-]+)$" }, + "participantRole": { "enum": ["primary", "secondary"] }, + "representative": { "type": "boolean" }, + "firstTimeUser": { "type": "boolean" }, + "conflict": { "enum": ["none", "maintainer", "contributor", "rubric-reviewer", "other"] }, + "synthetic": { "type": "boolean" }, + "consent": { + "oneOf": [ + { "type": "null" }, + { + "type": "object", + "additionalProperties": false, + "required": ["given", "at", "protocolVersion", "withdrawnAt"], + "properties": { + "given": { "const": true }, + "at": { "type": "string", "format": "date-time" }, + "protocolVersion": { "const": 1 }, + "withdrawnAt": { "type": ["string", "null"], "format": "date-time" } + } + } + ] + }, + "platform": { "$ref": "#/$defs/platform" }, + "firstUse": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/firstUse" }] }, + "comprehension": { "$ref": "#/$defs/comprehension" } + } + }, + "platform": { + "type": "object", + "additionalProperties": false, + "required": ["id", "os", "osVersion", "nodeVersion", "opencodeVersion", "browser", "browserVersion"], + "properties": { + "id": { "type": "string", "minLength": 1, "maxLength": 120 }, + "os": { "type": "string", "minLength": 1, "maxLength": 120 }, + "osVersion": { "type": "string", "minLength": 1, "maxLength": 120 }, + "nodeVersion": { "type": "string", "minLength": 1, "maxLength": 120 }, + "opencodeVersion": { "type": "string", "minLength": 1, "maxLength": 120 }, + "browser": { "type": "string", "minLength": 1, "maxLength": 120 }, + "browserVersion": { "type": "string", "minLength": 1, "maxLength": 120 } + } + }, + "firstUse": { + "type": "object", + "additionalProperties": false, + "required": ["startedAt", "endedAt", "elapsedSeconds", "completed", "readmeOnly", "repositoryCheckout", "hostingAccount", "maintainerAssistance", "failureStep"], + "properties": { + "startedAt": { "type": "string", "format": "date-time" }, + "endedAt": { "type": "string", "format": "date-time" }, + "elapsedSeconds": { "type": "number", "minimum": 0, "maximum": 3600 }, + "completed": { "type": "boolean" }, + "readmeOnly": { "type": "boolean" }, + "repositoryCheckout": { "type": "boolean" }, + "hostingAccount": { "type": "boolean" }, + "maintainerAssistance": { "type": "boolean" }, + "failureStep": { "type": ["string", "null"], "maxLength": 300 } + } + }, + "comprehension": { + "type": "object", + "additionalProperties": false, + "required": ["fixtureId", "artifactSha256", "startedAt", "elapsedSeconds", "maintainerAssistance", "answers", "scores"], + "properties": { + "fixtureId": { "type": "string", "minLength": 1, "maxLength": 80 }, + "artifactSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "startedAt": { "type": "string", "format": "date-time" }, + "elapsedSeconds": { "type": "number", "minimum": 0, "maximum": 600 }, + "maintainerAssistance": { "type": "boolean" }, + "answers": { "$ref": "#/$defs/fourFieldsText" }, + "scores": { "$ref": "#/$defs/fourFieldsBoolean" } + } + }, + "fourFieldsText": { + "type": "object", + "additionalProperties": false, + "required": ["purpose", "primaryFinding", "provenance", "nextAction"], + "properties": { + "purpose": { "type": "string", "minLength": 1, "maxLength": 500 }, + "primaryFinding": { "type": "string", "minLength": 1, "maxLength": 500 }, + "provenance": { "type": "string", "minLength": 1, "maxLength": 500 }, + "nextAction": { "type": "string", "minLength": 1, "maxLength": 500 } + } + }, + "fourFieldsBoolean": { + "type": "object", + "additionalProperties": false, + "required": ["purpose", "primaryFinding", "provenance", "nextAction"], + "properties": { + "purpose": { "type": "boolean" }, + "primaryFinding": { "type": "boolean" }, + "provenance": { "type": "boolean" }, + "nextAction": { "type": "boolean" } + } + } + } +} diff --git a/docs/license-dispositions.json b/docs/license-dispositions.json new file mode 100644 index 0000000..4de5e23 --- /dev/null +++ b/docs/license-dispositions.json @@ -0,0 +1,32 @@ +{ + "schemaVersion": 1, + "reviewedAt": "2026-08-16", + "approvedBy": "bitgorust", + "approvedAt": "2026-08-16T14:57:27Z", + "dispositions": [ + { + "path": "node_modules/khroma", + "version": "2.1.0", + "declaredLicense": null, + "selectedLicense": "MIT", + "licenseFile": "node_modules/khroma/license", + "sha256": "66b333b0f66759a0b710459e03f7029abe17f4358114a128d2c972e642961b49" + }, + { + "path": "node_modules/dompurify", + "version": "3.4.13", + "declaredLicense": "(MPL-2.0 OR Apache-2.0)", + "selectedLicense": "Apache-2.0", + "licenseFile": "node_modules/dompurify/LICENSE", + "sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + }, + { + "path": "node_modules/json-schema", + "version": "0.4.0", + "declaredLicense": "(AFL-2.1 OR BSD-3-Clause)", + "selectedLicense": "BSD-3-Clause", + "licenseFile": "node_modules/json-schema/LICENSE", + "sha256": "4e053c510d6f3e4724213a292c65142df68d069c40f558582bc4270914e16f77" + } + ] +} diff --git a/docs/page-quality-benchmark.md b/docs/page-quality-benchmark.md index 4f2a7bd..29d3def 100644 --- a/docs/page-quality-benchmark.md +++ b/docs/page-quality-benchmark.md @@ -42,9 +42,9 @@ Reference material is ranked to avoid building a target from cherry-picked third current supported CLI and Artifact feature. 2. **Official Anthropic material.** The [Artifact guide](https://code.claude.com/docs/en/artifacts), - [official viewer screenshot](references/claude-artifact-viewer.png), and [launch demonstration](https://www.youtube.com/watch?v=m7TJqx8CYG8) define the initial - task families and visible quality bar. + task families and visible quality bar. These are link-only references; their media is not + copied into this repository without recorded redistribution authority. 3. **Public community examples.** These may reveal useful patterns, but are supplemental because selection bias, unknown prompts, manual editing, and uncertain provenance make them unsuitable as pass/fail references. @@ -156,8 +156,8 @@ Artifact design change. ## Current status The repository has a useful renderer regression corpus under `examples/patterns/` and -`docs/evidence/patterns/`, and it retains one official viewer screenshot. That proves pattern -coverage and catches regressions; it is not a same-input comparative benchmark. +`docs/evidence/patterns/`; official visual material remains link-only. The local corpus proves +pattern coverage and catches regressions; it is not a same-input comparative benchmark. A visual audit of the closest dashboard pair found a concrete shortfall: the current OpenCode example renders a small fixed-size chart inside a wide card with a large unintended diff --git a/docs/product-spec.md b/docs/product-spec.md index 1d964d3..f8d765f 100644 --- a/docs/product-spec.md +++ b/docs/product-spec.md @@ -197,16 +197,24 @@ coverage map is [`docs/requirements-traceability.md`](requirements-traceability. work into a durable page. Reviewers and small teams are the secondary audience; organization administrators are the tertiary audience. When priorities conflict, preserve local authoring first, review second, and administration third without weakening security. -- **OUT-02:** A first-time user on a supported clean machine MUST be able to install the - released package, create an offline artifact, and reopen it by following only the README in - at most ten minutes, without a repository checkout or hosting account. -- **OUT-03:** In the checked-in journey corpus, at least 90% of at least ten representative - primary users MUST be able to identify the artifact's purpose, primary finding/state, - provenance, and next action within one minute without maintainer assistance. A collaboration - release additionally includes at least five representative secondary reviewers. -- **OUT-04:** Local artifact core, local collaboration, public snapshots, authenticated - collaboration, and connector-capable artifacts MUST remain separately releasable outcomes. - A release MUST meet every requirement assigned to its claimed level. +- **OUT-02:** A certified local-artifact-core or later release MUST show that a first-time user + on every claimed supported clean machine can install the exact package, create an offline + artifact, and reopen it by following only the README in at most ten minutes, without a + repository checkout or hosting account. An explicitly unsupported public preview MAY + distribute without this baseline only while recording it incomplete and making no first-use + or support claim. Synthetic agent probes are diagnostic and never participant evidence. +- **OUT-03:** A certified local-artifact-core or later release MUST show that, in the checked-in + journey corpus, at least 90% of at least ten representative primary users identify the + artifact's purpose, primary finding/state, provenance, and next action within one minute + without maintainer assistance. A collaboration certification additionally includes at least + five representative secondary reviewers. An explicitly unsupported public preview MAY + distribute without this baseline only while recording it incomplete and making no human- + comprehension claim; model runs remain synthetic diagnostics. +- **OUT-04:** Public preview is a non-certified distribution state that MAY precede the + separately releasable local artifact core, local collaboration, public snapshots, + authenticated collaboration, and connector-capable artifact levels. Public preview MUST be + labeled unsupported and uncertified and supplies no waiver or accumulated evidence to a + later level. A certified release MUST meet every requirement assigned to its claimed level. - **OUT-05:** Product-outcome measurement MUST use consented studies, local/CI benchmarks, or opt-in telemetry. The package MUST NOT send usage telemetry by default, and declining measurement MUST NOT reduce product functionality. @@ -510,10 +518,13 @@ coverage map is [`docs/requirements-traceability.md`](requirements-traceability. ## 11. Compatibility, portability, and migration requirements -- **COMPAT-01:** The support matrix MUST name tested Node, OpenCode, OS, desktop browser, and - mobile browser ranges. The target floor is Node 24+, current and oldest-supported stable +- **COMPAT-01:** The support matrix MUST distinguish exact tested, unverified, unsupported, and + supported Node, OpenCode, OS, desktop-browser, and mobile-browser ranges. Certified local- + artifact-core and later releases target Node 24+, current and oldest-supported stable OpenCode, current Ubuntu LTS, current and previous macOS, Windows 11, and the latest two - stable Chromium/Firefox/Safari releases where available. + stable Chromium/Firefox/Safari releases where available. Public preview MAY have zero + supported cells only when every release surface says so and narrow technical observations + are not promoted into support. - **COMPAT-02:** The portable HTML file is the long-term compatibility artifact. It MUST remain openable without this package, OpenCode, a service worker, an installed runtime, or a network connection on every supported browser. @@ -545,9 +556,13 @@ coverage map is [`docs/requirements-traceability.md`](requirements-traceability. - **DIST-02:** Registry, official OpenCode configuration, and local-development installation paths MUST be tested and documented from clean state. Published packages MUST remain usable across patch fixes unless a documented security issue requires otherwise. -- **DIST-03:** Releases MUST use SemVer and Conventional Commits, include generated or checked - release notes, state their release level/capabilities, name migrations and known limits, - and link the evidence supporting any parity or production-readiness claim. +- **DIST-03:** Every distribution MUST use SemVer and Conventional Commits, include generated + or checked release notes, state its distribution/certification status and capabilities, + name migrations and known limits, and link exact evidence. Public preview MUST pass build, + test, structural, package, final-byte security, vulnerability, license, redistribution, + private-intake, trusted-publishing, registry-integrity, signature, and provenance gates; it + MUST also expose missing OUT-02, OUT-03, support, parity, and production-readiness evidence. + Certified releases additionally meet every requirement for their claimed level. - **DIST-04:** Release CI MUST produce an SBOM and provenance/attestation for the packed bytes, scan runtime and development dependencies for known vulnerabilities and incompatible licenses, and document how consumers verify package integrity. @@ -586,9 +601,12 @@ coverage map is [`docs/requirements-traceability.md`](requirements-traceability. - **QUAL-07:** A page-quality claim requires the dated prompts, fixtures, all required runs, interaction traces, hard-gate results, blinded rubric distributions, reference environment, and authorization metadata defined in `docs/page-quality-benchmark.md`. -- **QUAL-08:** Release evidence MUST state failures, exclusions, flaky-test disposition, and - unsupported platforms as visibly as successes. Documentation and the README capability - matrix MUST be checked against this requirement set before release. +- **QUAL-08:** Distribution evidence MUST state failures, exclusions, flaky-test disposition, + unsupported platforms, and uncollected human evidence as visibly as successes. + Documentation and README claims MUST be checked against this requirement set before every + distribution. Missing OUT-02, OUT-03, and support evidence is permitted only for explicitly + unsupported, uncertified public preview and remains incomplete rather than passed or not + applicable. ## 14. Definition of complete diff --git a/docs/redistribution-inventory.json b/docs/redistribution-inventory.json new file mode 100644 index 0000000..0a40c28 --- /dev/null +++ b/docs/redistribution-inventory.json @@ -0,0 +1,243 @@ +{ + "schemaVersion": 1, + "reviewedAt": "2026-08-16", + "repositoryLicense": "MIT", + "authoredScopes": [ + { "path": ".github/", "provenance": "repository-authored", "license": "MIT" }, + { "path": ".gitignore", "provenance": "repository-authored", "license": "MIT" }, + { "path": "AGENTS.md", "provenance": "repository-authored", "license": "MIT" }, + { "path": "LICENSE", "provenance": "repository-authored", "license": "MIT" }, + { "path": "README.md", "provenance": "repository-authored", "license": "MIT" }, + { "path": "SECURITY.md", "provenance": "repository-authored", "license": "MIT" }, + { "path": "agents/", "provenance": "repository-authored", "license": "MIT" }, + { "path": "docs/", "provenance": "repository-authored", "license": "MIT" }, + { "path": "examples/", "provenance": "repository-authored", "license": "MIT" }, + { "path": "package-lock.json", "provenance": "repository-authored", "license": "MIT" }, + { "path": "package.json", "provenance": "repository-authored", "license": "MIT" }, + { "path": "scripts/", "provenance": "repository-authored", "license": "MIT" }, + { "path": "skills/", "provenance": "repository-authored", "license": "MIT" }, + { "path": "src/", "provenance": "repository-authored", "license": "MIT" }, + { "path": "test/", "provenance": "repository-authored", "license": "MIT" }, + { "path": "tsconfig.json", "provenance": "repository-authored", "license": "MIT" } + ], + "binaryAssets": [ + { + "path": "docs/evidence/artifact-page.png", + "sha256": "0a17e53478d86bb23c8624add368da6632f2cad34efd2458762338cfe681efc9", + "provenance": "repository-generated browser capture", + "source": "repository-authored synthetic incident-report fixture and renderer", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/comments.png", + "sha256": "569f3ff773b00040ff4250eb9cd6f319f534d01d0f933a9d2220c72a3ea651cc", + "provenance": "repository-generated browser capture", + "source": "repository-authored comments surface with synthetic content", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/gallery.png", + "sha256": "916f9b90b446f9968c4293829d33651272a936761803b2e997af71ecc28dc8dd", + "provenance": "repository-generated browser capture", + "source": "repository-authored gallery with synthetic artifacts", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/governance/renderer-upgrade-chromium-2026-08-16.png", + "sha256": "f61b86a00cd93a73fcc06f2947bb1f38def19c20328e4e944528ee1b32981951", + "provenance": "repository-generated browser capture", + "source": "examples/patterns/tune-controls.md and documented adversarial smoke", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/hosted-worker.png", + "sha256": "edff691758c877801de3b0d77d519ddc9dd15ec271c967e4d0614192f520e2f9", + "provenance": "repository-generated browser capture", + "source": "user-operated test deployment of a repository-authored synthetic fixture", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/hosted-workshop.png", + "sha256": "a6451f5fbcc87cdbeebe17eb5180bb4813d308ec85042a0406a084db58337a03", + "provenance": "repository-generated browser capture", + "source": "user-operated test deployment of examples/patterns/workshop.md", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/live-artifact.png", + "sha256": "edff691758c877801de3b0d77d519ddc9dd15ec271c967e4d0614192f520e2f9", + "provenance": "repository-generated browser capture", + "source": "repository-authored live artifact with synthetic content", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/live-gallery.png", + "sha256": "1f66acac18a19f7af420c8e48250031da43216cfd9ee2248346c58eab4a30402", + "provenance": "repository-generated browser capture", + "source": "repository-authored live gallery with synthetic artifacts", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/patterns/compare-layouts.png", + "sha256": "8d4847f887462c6953067b0dd4bd2ca5faf692e9ac5d7224f0188225a8639784", + "provenance": "repository-generated browser capture", + "source": "examples/patterns/compare-layouts.md", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/patterns/dashboard.png", + "sha256": "62f848af2e23c26493d3f9e24b6af4e59516e8d9ce1e0fd2b68169dfc98281dc", + "provenance": "repository-generated browser capture", + "source": "examples/patterns/dashboard.md", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/patterns/funnel-editorial.png", + "sha256": "5b5c0a84ad004d4cb8a15bf806d8534f432f63021051fde073ade896ca0f6666", + "provenance": "repository-generated browser capture", + "source": "examples/patterns/funnel-analysis.md editorial theme", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/patterns/funnel-ops.png", + "sha256": "24a27c29a6f1b9bf09e760ad778662e3c260aa56d6b8fa0c735f414b1740bc73", + "provenance": "repository-generated browser capture", + "source": "examples/patterns/funnel-analysis.md ops theme", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/patterns/funnel-report.png", + "sha256": "f618a5ad36a7c037338e17714d7a1510eaa21b4c4bbb816de7bf55030ffeec2e", + "provenance": "repository-generated browser capture", + "source": "examples/patterns/funnel-analysis.md report theme", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/patterns/incident.png", + "sha256": "e9711c045dbf0bf8feaeba0515a68bc922e428b002e175766deb273d6bdb436c", + "provenance": "repository-generated browser capture", + "source": "examples/patterns/incident.md", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/patterns/license-audit.png", + "sha256": "3b0fef251ca06f84288571b9bcbcc4c89664bdcab28182daa24b9b277981769c", + "provenance": "repository-generated browser capture", + "source": "examples/patterns/license-audit.md", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/patterns/plan.png", + "sha256": "f9c45f52c16dcfb5e9b6b12adfe33363d281d2ddd168e9487bac5570f797b541", + "provenance": "repository-generated browser capture", + "source": "examples/patterns/plan.md", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/patterns/pr-walkthrough.png", + "sha256": "0214823d65bab1852a5ea48a0076a8eaebc82a88d944ee26edfe35da29eb1c06", + "provenance": "repository-generated browser capture", + "source": "examples/patterns/pr-walkthrough.md", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/patterns/release-checklist.png", + "sha256": "248835c3c195360cc7be6f4555972fe1ea65d64e52cb54cee9bdcccbc22d9cb7", + "provenance": "repository-generated browser capture", + "source": "examples/patterns/release-checklist.md", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/patterns/tune-controls.png", + "sha256": "704e911c81da45be5cd89300f5d621635294aaa2a35c25254b915d5dbd383e7b", + "provenance": "repository-generated browser capture", + "source": "examples/patterns/tune-controls.md", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/patterns/workshop.png", + "sha256": "72a7125f6271576c57340dfc16dc11c0cf64d3d65174b8ab4b645ce4630dbcd0", + "provenance": "repository-generated browser capture", + "source": "examples/patterns/workshop.md", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + }, + { + "path": "docs/evidence/theme-toggle-dark.png", + "sha256": "b495dfbb3bc4a8c5b3e7a445cd63e5eb32f3e59ff3f857f0c1d087796033b742", + "provenance": "repository-generated browser capture", + "source": "repository-authored synthetic page and renderer dark-theme control", + "license": "MIT", + "attribution": "opencode-artifacts contributors", + "status": "approved" + } + ], + "embeddedFonts": [], + "externalBenchmarkReferences": [ + { + "url": "https://code.claude.com/docs/en/artifacts", + "role": "official current feature and visual reference", + "localCopy": null, + "redistribution": "link-only" + }, + { + "url": "https://www.youtube.com/watch?v=m7TJqx8CYG8", + "role": "official launch demonstration used as historical visual context", + "localCopy": null, + "redistribution": "link-only" + } + ], + "removedMaterials": [ + { + "path": "docs/references/claude-artifact-viewer.png", + "sha256": "5784a8cdc227f204a8b7ebf9a5e6cf4056170db17d2ea96b85f11a1aba82bdf3", + "source": "official Anthropic viewer screenshot", + "reason": "no explicit redistribution license was established; use the official link instead", + "status": "removed" + } + ], + "dependencyEvidence": [ + "package-lock.json", + "docs/license-dispositions.json", + "docs/evidence/governance/renderer-remediation-2026-08-16.md" + ] +} diff --git a/docs/redistribution-policy.md b/docs/redistribution-policy.md new file mode 100644 index 0000000..ef06ebb --- /dev/null +++ b/docs/redistribution-policy.md @@ -0,0 +1,24 @@ +# Redistribution and attribution inventory + +Policy version: 1. Last reviewed: 2026-08-16. + +The machine-readable [`redistribution-inventory.json`](redistribution-inventory.json) is the +complete Phase 0 disposition for repository documentation, examples, retained binary assets, +fonts, benchmark references, and dependency evidence. Repository-authored source, policy, +documentation, fixtures, skills, tests, and generated screenshots are distributed under the +root [MIT license](../LICENSE). Every retained binary asset is bound to exact bytes by SHA-256 +and names its source and attribution. + +No font file is embedded or redistributed. The renderer uses system font-family fallbacks. +Runtime dependency terms and the three exceptional branch choices are governed by +[`license-dispositions.json`](license-dispositions.json) and the exact candidate evidence in +[`renderer-remediation-2026-08-16.md`](evidence/governance/renderer-remediation-2026-08-16.md). + +Official Anthropic material and the launch video are link-only benchmark references. The +previous local copy of an official viewer screenshot was removed because no explicit +redistribution license was established. Current same-input Claude outputs remain prohibited +from the repository unless their capture and redistribution authority are recorded first. + +The repository check scans retained media, document, video, audio, and font extensions. An +unknown asset, changed digest, missing provenance, local copy of a link-only reference, or +font without a new exact disposition fails closed. diff --git a/docs/references/claude-artifact-viewer.png b/docs/references/claude-artifact-viewer.png deleted file mode 100644 index 61205d7..0000000 Binary files a/docs/references/claude-artifact-viewer.png and /dev/null differ diff --git a/docs/release-evidence-template.md b/docs/release-evidence-template.md index b9599af..6ed26e3 100644 --- a/docs/release-evidence-template.md +++ b/docs/release-evidence-template.md @@ -4,8 +4,10 @@ Status: Draft / Passed / Failed / Withdrawn Decision date: YYYY-MM-DD -Release level: local artifact core / local collaboration / public snapshots / authenticated -collaboration / connector-capable artifacts +Distribution status: public preview / certified + +Certified release level: none (public preview) / local artifact core / local collaboration / +public snapshots / authenticated collaboration / connector-capable artifacts Release owner: `` @@ -16,6 +18,7 @@ them here. ## Claim and scope - User outcome being released: +- Preview/support/certification label shown to users: - Archived change packets included in this release: - Included capability classes: - Explicitly unavailable capabilities: @@ -24,6 +27,11 @@ them here. - Support and security-fix window: - Known limitations: +For public preview, OUT-02, OUT-03, supported-platform, parity, and production-readiness rows +remain `incomplete` or `unverified`, never pass or N/A. Public preview must say unsupported and +uncertified everywhere it is summarized. For certification, resolve every applicable row +under the selected accumulated release level. + ## Requirements decision Use one row per requirement or an inclusive range only when every ID has the same result and @@ -60,6 +68,29 @@ npm run check Record skipped, flaky, retried, quarantined, and platform-specific failures here: +## Public-preview transition (required for public preview) + +| Pre-publish hard gate | Pass / Fail | Exact evidence | +|---|---|---| +| tests | | | +| build | | | +| structural checks | | | +| exact package contents/coordinate | | | +| final secret scan and CSP | | | +| vulnerability audit | | | +| license disposition | | | +| redistribution inventory | | | +| private vulnerability intake | | | +| exact npm trusted-publisher binding | | | + +| Post-publish hard gate | Pass / Fail | Exact evidence | +|---|---|---| +| registry integrity equals packed bytes | | | +| registry package signature | | | +| registry provenance for exact tag/commit/workflow | | | + +Transition result: **development / preview-candidate / public-preview / failed** + ## Browser, accessibility, and page quality - Desktop/mobile/color-mode/browser matrix: @@ -106,10 +137,12 @@ reduce the claim rather than converting it to a pass. ## Supply-chain outputs -- Packed filename, digest, and registry provenance: -- SBOM: -- Attestation/signature verification: -- Dependency/license/vulnerability reports: +- Packed filename, SHA-256, SRI, tag/commit/workflow, and registry integrity cross-check: +- CycloneDX JSON from the exact lockfile/install: +- Registry trusted-publisher status and published provenance URL/verification (configuration is not evidence): +- Registry signature verification: +- Dependency vulnerability report and disposition: +- SPDX license/attribution inventory and disposition: - Release notes and migration guide: ## Decision, rollout, and support @@ -121,7 +154,7 @@ reduce the claim rather than converting it to a pass. - Post-release checks and monitoring window: - Incident/support contacts: -Final decision: **Pass / Fail / Reduce claim** +Final decision: **Public preview / Certified pass / Fail / Reduce claim** Accountable release owner: diff --git a/docs/release.md b/docs/release.md index a09a3d4..4ae4f60 100644 --- a/docs/release.md +++ b/docs/release.md @@ -8,8 +8,9 @@ automatically (public repo + public package). The package was first published manually (`npm publish --otp`), then the trusted publisher was linked on npmjs.com (GitHub Actions: `bitgorust/opencode-artifacts`, workflow -`publish.yml`, action `npm publish`). These steps never need repeating; they are recorded -here for forks: +`publish.yml`, action `npm publish`). An authenticated `npm trust list` readback verified that +exact repository/workflow binding and `createPackage` permission on 2026-08-16. These setup +steps do not normally need repeating; they are recorded here for forks: 1. First publish is manual (the package must exist before a trusted publisher can be linked): diff --git a/docs/requirements-traceability.md b/docs/requirements-traceability.md index 2c2a038..a464df1 100644 --- a/docs/requirements-traceability.md +++ b/docs/requirements-traceability.md @@ -2,7 +2,7 @@ Status: Normative planning control -Last reviewed: 2026-08-15 +Last reviewed: 2026-08-16 This map connects every requirement in [`docs/product-spec.md`](product-spec.md) to delivery, ownership, and acceptance. Ranges are inclusive: a row such as `OUT-01`–`OUT-06` assigns every @@ -24,7 +24,7 @@ not-applicable. | Requirements | Owning perspective | Roadmap / release gate | Accountable role | Evidence contract | Current status | |---|---|---|---|---|---| -| `OUT-01`–`OUT-06` | Product outcomes | Phase 0 and every release gate | Product maintainer | named-user journey study, outcome metrics, release-level checklist, phase planning fields | Partial | +| `OUT-01`–`OUT-06` | Product outcomes | Phase 0 preview contract; Goal 5 and every certification gate | Product maintainer | honest incomplete preview status; named-user journey study, outcome metrics, certification checklist, phase planning fields | Partial | | `UX-01`–`UX-08` | Human workflow | Phases 0–6; applicable release gate | Product maintainer | end-to-end journey corpus, error/permission/teardown review, Unicode/RTL/zoom QA | Partial | | `OC-01`–`OC-06` | OpenCode integration | Phase 3; local artifact core | OpenCode integration maintainer | clean packed-host matrix, tool/skill discovery, permission denial tests | Partial | | `LIFE-01`–`LIFE-07` | Artifact lifecycle | Phase 1; local artifact core | Core storage maintainer | migration fixtures, multi-process race and fault-injection tests, CLI/plugin lifecycle E2E | Partial | @@ -39,8 +39,8 @@ not-applicable. | `PRIV-01`–`PRIV-07` | Data purpose and rights | Phase 0 policy; Phases 5–6 implementation; every release gate | Privacy maintainer | data inventory, no-telemetry proof, export/delete test, retention and public-abuse policy | Missing | | `OPS-01`–`OPS-08` | Reliability and operation | Phases 1, 4, 5B, and 6; applicable release gate | Operations maintainer | SLO dashboard/report, restore drill, degraded-mode/rollout/incident runbooks, quota alerts | Missing | | `PERF-01`–`PERF-07` | Speed, capacity, and cost | Phases 2, 4, 5B, and 6; applicable release gate | Performance maintainer | reproducible percentile benchmarks, limits, load/soak report, provider cost model | Missing | -| `COMPAT-01`–`COMPAT-08` | Platforms and evolution | Phases 1 and 3; hosting migration in Phase 5B; every release gate | Compatibility maintainer | support matrix CI/manual QA, schema fixtures, export round trip, upgrade/rollback tests | Partial | -| `DIST-01`–`DIST-07` | Release and supply chain | Phase 0 policy, Phase 3 automation, every release gate | Release maintainer | packed-host test, clean installs, release checklist, SBOM/provenance/license/vulnerability output | Partial | +| `COMPAT-01`–`COMPAT-08` | Platforms and evolution | Phase 0 preview disclosure; Phases 1 and 3; hosting migration in Phase 5B; every certification gate | Compatibility maintainer | exact tested/unverified preview matrix; support matrix CI/manual QA, schema fixtures, export round trip, upgrade/rollback tests | Partial | +| `DIST-01`–`DIST-07` | Release and supply chain | Phase 0 preview gate, Phase 3 automation, every distribution/certification gate | Release maintainer | preview transition model, packed-host test, clean installs, release checklist, SBOM/provenance/license/vulnerability output | Partial | | `QUAL-01`–`QUAL-08` | Acceptance evidence | Every phase and release gate | Quality maintainer | traceability check, automated/manual suites, retained evidence, honest failure report | Partial | ## Release applicability @@ -51,6 +51,7 @@ not omitted. | Release level | Required product behavior | Additional release-specific IDs | |---|---|---| +| Public preview (non-certified distribution) | Exact package/security/privacy/supply-chain hard gates; unsupported/uncertified label; missing human, platform, parity, and production evidence visible | `OUT-04`, `COMPAT-01`, `DIST-03`–`DIST-07`, `QUAL-01`, `QUAL-02`, `QUAL-06`, `QUAL-08`, and applicable `SEC`/`PRIV`; OUT-02/OUT-03 remain incomplete | | Local artifact core | `OUT`, `UX`, `OC`, `LIFE`, `RENDER`, `LOCAL-01`, plus applicable `SEC`, `PRIV`, `OPS`, `PERF`, `COMPAT`, `DIST`, `QUAL` | None | | Local collaboration | Local artifact core + `LOCAL-02`–`LOCAL-05` | Local-service security, privacy, operations, performance, and quality cases | | Public snapshots | Local collaboration + public hosting | `HOST-01`, `HOST-02`, `HOST-08`, `HOST-10` and applicable portions of `HOST-07` | @@ -66,14 +67,17 @@ results. A release checklist resolves each class to dated files and command outp |---|---| | Unit/integration tests | `test/` and CI output | | Structural/release assertions | `scripts/check-repo.ts`, `scripts/checks.ts`, CI, `npm pack --dry-run` | +| Contract link integrity | `scripts/check-links.ts`, `test/documentation-links.test.ts`, dated `docs/evidence/contract/` reports | +| Journey and outcome studies | `docs/journeys/`, `scripts/journey-study.ts`, `test/journey-study.test.ts`, dated `docs/evidence/journeys/` reports | | Browser and visual evidence | `docs/evidence/` | | Page-quality comparison | `docs/page-quality-benchmark.md`, dated `docs/evidence/page-quality/` reports | | OpenCode/Claude host probes | `docs/evidence/opencode-host-verification.md`, `docs/evidence/claude-code-host-verification.md` | -| Threat model and security cases | planned `docs/threat-model.md`, security-focused tests | +| Threat model and security cases | `docs/threat-model.md`, `docs/security.md`, `SECURITY.md`, security-focused tests | | Performance/capacity/cost | planned `docs/evidence/performance/` reports and reference-host cost model | -| Privacy/data governance | planned data inventory, retention/deletion and public-abuse policies | +| Privacy/data governance | `docs/data-governance.md`, `docs/governance-policy.json`, dated governance reports | | Operations | planned runbooks and dated restore/rollout/incident drills | | Release decision | `docs/release-evidence-template.md`; completed copies under `docs/evidence/releases/` | +| Platform and supply-chain support | `docs/support-policy.md`, `docs/governance-policy.json`, dated `docs/evidence/governance/` reports | ## Change control diff --git a/docs/roadmap.md b/docs/roadmap.md index 1c8a74a..d99aee6 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -56,26 +56,29 @@ Evidence from the repository and clean local host probes: | Authenticated team sharing | Missing | Cloudflare Access is a manual guide, not verified/configured product behavior; no roles, audience UI, or revocation model | | Hosted live updates | Missing | already-open hosted pages are not notified of a new head | | Viewer-scoped MCP connectors | Missing | local fixed-command datasource bridge is not a hosted per-viewer connector broker | -| Governance | Missing | no retention policy, deletion lifecycle, compliance API, or artifact audit log | +| Governance | Partial | Phase 0 security/data/support policies exist; authenticated lifecycle API, production evidence, and artifact audit log remain missing | | Packaged-host compatibility CI | Missing | unit plugin tests exist; the packed tarball is not loaded into a clean current OpenCode host in CI | | Product outcomes and usability evidence | Missing | target users and measurable journeys are now specified, but no journey study has been run | -| Privacy/operations/performance governance | Missing | requirements and owners exist; inventories, runbooks, harnesses, SLO evidence, and cost models do not | -| Supply-chain release evidence | Partial | lockfile and pack inspection exist; SBOM, provenance, license/vulnerability evidence, and support policy do not | +| Privacy/operations/performance governance | Partial | privacy inventory/policy exists; operations runbooks, SLO evidence, performance harnesses, and cost models do not | +| Supply-chain release evidence | Pass for public preview | exact 0.14.4 audit, licenses, redistribution, trusted-publisher readback, packed/registry bytes, signature, SBOM, and tag/commit/workflow provenance pass; certification remains unclaimed | “Shipped” means the current behavior exists; it does not waive any stronger acceptance rule in the product spec. ## Phase 0 — Make the contract truthful -Goal: one authoritative, measurable definition and no inflated capability language. +Goal: one authoritative, measurable definition and an executable unsupported public-preview +gate with no inflated capability language. Requirements: `OUT-01` through `OUT-06`, `UX-01` through `UX-08` at contract level, `PRIV-01` through `PRIV-07` at policy level, `SEC-01`, `SEC-10`, `COMPAT-01`, `DIST-03` through `DIST-07`, `QUAL-01`, `QUAL-08`. Owner: Product/release maintainer. Effort: **M**. Dependencies: none. Main risk: producing -paper policy without executable evidence. Stop/re-scope: do not start a release claim whose -users, support envelope, data handling, or acceptance evidence cannot be named. +paper policy without executable evidence or mistaking public distribution for certification. +Stop/re-scope: do not start a certified release claim whose users, support envelope, data +handling, or acceptance evidence cannot be named; a public preview must expose every missing +certification input and claim zero support. - [x] Establish `docs/product-spec.md` as the normative target. - [x] Separate portable artifact behavior from optional service behavior. @@ -85,20 +88,30 @@ users, support envelope, data handling, or acceptance evidence cannot be named. account-authentication boundary separately from verified behavior. - [x] Add a release-evidence template that names the claimed level and resolves every requirement to evidence, failure, or reasoned non-applicability. -- [ ] Add a documentation link checker for official source URLs and internal spec anchors. +- [x] Add a documentation link checker for official source URLs and internal spec anchors. - [x] Define a MECE requirement taxonomy and map every requirement to a phase, owner role, evidence contract, release applicability, and status. - [x] Adopt a risk-scaled spec-anchored workflow that separates target intent, current shipped behavior, proposed deltas, and validation/verification evidence. -- [ ] Check in the create/revise/review/share journey corpus and run the first OUT-02/OUT-03 - study with consented representative users. -- [ ] Publish the supported-platform matrix, threat model, data inventory, telemetry stance, +- [x] Check in the create/revise/review/share journey corpus, consent protocol, strict study + schema, and diagnostic harness; record OUT-02/OUT-03 as incomplete until a future + certification owner elects to run the representative-user study. +- [x] Publish the supported-platform matrix, threat model, data inventory, telemetry stance, retention/deletion/public-abuse policies, vulnerability contact/response policy, and supported-version/deprecation policy. +- [x] Define a machine-checked public-preview state that requires exact security, privacy, + redistribution, package, trusted-publishing, and provenance evidence while refusing support, + usability, parity, or production-readiness claims. Exit gate: README, comparison, component docs, hosting docs, traceability, and release template agree on what is shipped, partial, missing, or not applicable; every normative ID -has one owner and evidence path; the first-use and comprehension baselines are recorded. +has one owner and evidence path; the public-preview transition model rejects every failed hard +gate and keeps first-use, comprehension, and target-platform evidence visibly incomplete. +Human outcome and full support-matrix results gate future certification, not Phase 0 or public +preview distribution. + +Status: **passed for the `v0.14.4` unsupported public preview**. This is not local-artifact-core +certification. ## Phase 1 — Durable artifact identity and transactions @@ -351,12 +364,12 @@ consequences, owner, and review trigger. “Use the expected shape” is not a d | ID | Decision and deadline | Owner role | Blocks | Current state | |---|---|---|---|---| -| `D-01` | Supported OpenCode/Node/OS/browser matrix before Phase 1 migration fixtures and Phase 3 CI | Compatibility maintainer | Phases 1, 3 | Open | +| `D-01` | Supported OpenCode/Node/OS/browser matrix before certification; preview may retain zero supported cells | Compatibility maintainer | Certification and platform default-enablement | Target published; exact cells unverified | | `D-02` | Authorized Claude benchmark account, model/settings protocol, artifact retention permission, and reviewer recruitment before Phase 2 comparison | Product/design maintainer | Equal-or-better claim | Blocked on account and reviewers | | `D-03` | Authenticated reference architecture: identity proxy, unreachable origin, revision store, strongly consistent mutable store, event delivery, backups, and regional availability before Phase 5B implementation | Hosting/identity maintainer | Phase 5B | Open | | `D-04` | Hosted operator/controller roles, data regions, retention defaults, deletion/backup expiry, abuse/takedown, and support/SLO policy before Phase 5B public preview | Privacy/operations maintainer | Phase 5B release | Open | | `D-05` | Connector provider/grant model, allowed protocol surface, SSRF boundary, cache policy, quotas, billing guardrails, and action eligibility before Phase 6 | Connector/security maintainer | Phase 6 | Open | -| `D-06` | Package provenance/signing mechanism, SBOM format, vulnerability/license policy, and supported-version window before the next production-readiness claim | Release maintainer | Release gate | Open | +| `D-06` | Package provenance/signing mechanism, SBOM format, vulnerability/license policy, and supported-version window before public preview or any production-readiness claim | Release maintainer | Distribution gate | Selected and verified for `v0.14.4` public preview | ## Risk register @@ -376,7 +389,18 @@ work or an explicit scope reduction; it cannot be accepted by omitting its evide | Platform/API churn breaks clean installation | High / High | current or oldest-supported packed-host job fails | explicit matrix, adapter boundary, narrow claims, tested deprecation/migration | Compatibility maintainer | | Supply-chain compromise reaches a release | Low / Critical | provenance, vulnerability, or integrity verification fails | pin/review, SBOM/attestation, rotation/revocation playbook, block release | Release/security maintainer | -## Recurring release gate — prove and support the claimed level +## Public-preview distribution gate + +Public preview is unsupported and uncertified. It may proceed with OUT-02, OUT-03, target +platform, comparative-quality, performance, and operational certification evidence visibly +incomplete, but only after every closed hard gate in `DIST-03` passes for exact candidate +bytes. The pre-publish transition requires tests, build, structural checks, package review, +final-byte secret/CSP controls, vulnerability/license/redistribution disposition, verified +private intake, and the exact trusted-publisher binding. The post-publish transition requires +registry byte equality, signature, and provenance. A failed hard gate blocks or fails the +preview; a prior preview supplies no waiver to certification. + +## Recurring certification gate — prove and support the claimed level Requirements: `OUT-04` through `OUT-06`, all applicable `UX`, `SEC`, `PRIV`, `OPS`, `PERF`, `COMPAT`, and `QUAL` requirements, plus `DIST-01` through `DIST-07`. @@ -407,6 +431,7 @@ Use these labels in release notes and README claims: | Level | Required phases | Claim allowed | |---|---|---| +| Public preview (not certified) | Phase 0 preview gate | Public inspection of explicitly unsupported exact package/source bytes; no capability-level claim | | Local artifact core | 0–3 | Offline pages and native local OpenCode lifecycle | | Local collaboration | 0–4 | Reliable served comments/decisions/live data on one machine | | Public snapshots | 0–4 + 5A | Explicit public static sharing | diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..c411283 --- /dev/null +++ b/docs/security.md @@ -0,0 +1,43 @@ +# Security model and response policy + +Policy version: 1. Last reviewed: 2026-08-16. + +The versioned [threat model](threat-model.md) and public [reporting policy](../SECURITY.md) +govern security claims. GitHub private vulnerability reporting was enabled with explicit +authorization and verified from the provider on 2026-08-16, so sensitive reports have a +private advisory path. That control alone is not a production-readiness claim. + +## Severity and response targets + +Use impact plus exploitability, with CVSS as supporting input rather than an automatic result: + +| Severity | Examples | Acknowledge / initial triage target after an accountable owner accepts the release | +|---|---|---| +| Critical | broad arbitrary code execution, credential theft, authentication bypass, malicious published package | 1 / 2 business days | +| High | scoped code execution, cross-audience private-data disclosure, privilege escalation, reliable supply-chain compromise | 2 / 5 business days | +| Medium | constrained disclosure/integrity loss requiring unusual conditions, meaningful denial of service | 5 / 10 business days | +| Low | limited hardening defect with no demonstrated confidentiality/integrity impact | 10 / 20 business days | + +These are targets, not a service-level guarantee. Private intake is operational; the response +targets additionally require a release to name an accountable security owner. Acknowledgment +does not promise a fix. The owner validates affected versions, avoids public exploit details, +agrees a disclosure date with the reporter when possible, and records severity changes. + +## Containment and recovery + +For a suspected credential or release compromise: + +1. pause publishing and affected deployment automation; +2. preserve minimally necessary private evidence and identify the exact affected authority, + package versions, commits, digests, provider targets, and audience; +3. revoke or rotate only the affected GitHub/npm/provider credentials, OIDC environment or + trusted-publisher configuration; user-owned Cloudflare/GitHub target credentials remain + the user's responsibility, with project guidance scoped to the affected integration; +4. deprecate affected package versions and publish bounded upgrade/containment guidance; +5. rebuild from a reviewed commit, repeat tests/SBOM/audit/license/package checks, and verify + the registry attestation and digest; and +6. resume only after the accountable security and release owners record the recovery result. + +Never paste tokens, private advisory content, raw participant data, or private artifacts into +public issues, diagnostics, fixtures, or release evidence. Secret scanning reduces accidental +exposure but is not exhaustive; final audience-bound bytes and staged metadata require review. diff --git a/docs/support-policy.md b/docs/support-policy.md new file mode 100644 index 0000000..dcefeb3 --- /dev/null +++ b/docs/support-policy.md @@ -0,0 +1,102 @@ +# Support and release policy + +Policy version: 1. Last reviewed: 2026-08-16. + +This document owns the Phase 0 D-01 platform decision and D-06 release/supply-chain decision. +The machine-readable status record is [`governance-policy.json`](governance-policy.json). + +## Current support status + +There are currently **no fully supported platform/browser cells**. A dated Ubuntu observation +installed an exact CI tarball, discovered all tools through real OpenCode, rendered and +reopened an offline page in one Chromium build, and repeated the reopen after package-tree +removal. It did not exercise Firefox, the latest two stable browser generations, or a +consented first-time user following only the README. The target matrix therefore remains +unverified and blocks a production-readiness claim. + +Public preview is nevertheless distributable after its closed technical, security, privacy, +package-integrity, and supply-chain gates pass. Public preview is explicitly unsupported and +uncertified: it makes no first-use, comprehension, parity, production-readiness, or supported- +platform claim. Narrow observations remain diagnostic. A certified local-artifact-core or +later release still requires the complete target matrix and OUT-02/OUT-03 evidence. + +| Cell | Target | Current status | Evidence or missing scope | +|---|---|---|---| +| Ubuntu technical observation | Ubuntu 24.04.4, Node 24.19.0, OpenCode 1.18.18, Chrome for Testing 145.0.7632.6 | Tested, not supported | [Exact packed observation](evidence/governance/ubuntu-packed-observation-2026-08-16.md); Firefox/latest-two/human first-use absent | +| Ubuntu desktop | Ubuntu 24.04 LTS, Node 24, stable OpenCode 1.x, latest two Chromium/Firefox | Unverified | Exact combined run absent | +| macOS desktop | Current and previous macOS, Node 24, stable OpenCode 1.x, latest two Safari/Chromium/Firefox | Unverified | Machines/runs absent | +| Windows desktop | Windows 11 native and WSL 2, Node 24, stable OpenCode 1.x, latest two Chromium/Firefox | Unverified | Machines/runs absent | +| Mobile viewer | Current/previous iOS Safari and current Android Chrome, latest two stable generations | Unverified | Devices/runs absent | +| Node before 24 | Any | Unsupported | Outside D-01 support floor | + +“Current” and “latest two” are moving targets, not version evidence. A release owner resolves +them to exact versions on the test date. A cell becomes supported only when one dated record +contains the exact OS, Node, OpenCode, browser/device, packed package digest, test scope, and +result. Failed evidence makes a cell unsupported; missing or stale evidence keeps it +unverified. Family resemblance and CI configuration do not promote a cell. + +The initial current and oldest-supported OpenCode versions are the same exact stable 1.x +version once a complete cell passes. A second host version is not claimed until it receives +the same packed-host coverage. Re-run a cell when its OS/browser generation changes, the Node +or OpenCode support line changes, a relevant runtime/renderer dependency changes, or a defect +shows the evidence no longer represents users. + +## Package version support + +Only the current package minor receives security-fix support. Older minors and all prerelease +or unpublished commits are unsupported; users should upgrade to the newest release in the +current minor. Broader concurrent-minor support requires a separately approved staffed test +matrix. + +Deprecation receives notice in at least one supported release, including replacement and +migration guidance. Maintainers may remove or disable behavior immediately when continued +notice would extend an active exploit; release notes must record the security exception +without disclosing usable exploit detail. End-of-life begins when a newer minor becomes the +supported minor. Unsupported versions receive no fix promise. + +## Distribution and certification gates + +Every distribution uses [the release evidence template](release-evidence-template.md) and +names either `public preview` or one certified capability level. It must include SemVer tag/ +version agreement, Conventional Commit history, reviewed notes, migrations, known limits/ +failures, and exact evidence for every claim. Missing evidence narrows or fails a certified +claim. For public preview, missing OUT-02, OUT-03, support, parity, and production evidence +must remain explicitly incomplete rather than becoming pass or not applicable. + +Public preview uses two transitions. `preview-candidate` passes before registry mutation only +when tests, build, structural checks, package review, final secret/CSP controls, audit, +licenses, redistribution, private intake, and exact trusted-publisher binding pass. `public- +preview` passes only after registry integrity, package signature, and provenance are verified +for those exact bytes. Any failed hard gate blocks or fails the preview. A prior preview +supplies no waiver or evidence to later certification. + +The D-06 supply-chain decision follows npm's official +[`npm sbom`](https://docs.npmjs.com/cli/commands/npm-sbom/), +[trusted publishing](https://docs.npmjs.com/trusted-publishers/), and +[provenance verification](https://docs.npmjs.com/viewing-package-provenance/) contracts and +requires all of the following for the exact packed bytes: + +- public GitHub Actions OIDC trusted publishing to npm, with registry provenance verified + after publication; +- CycloneDX JSON generated by npm from the exact lockfile/install; +- `npm audit`, registry signature verification, and an SPDX license disposition; +- package filename, SHA-256, SRI/integrity, tag, commit, workflow run, and registry version + cross-checked as the same release; and +- attribution for source, dependencies, documentation, examples, embedded assets/fonts, and + public benchmark references. + +The [dated provider report](evidence/governance/provider-status-2026-08-16.md) verifies both +the exact trusted-publisher repository/workflow binding and provenance for the prior published +release. That prior attestation cannot cover future candidate bytes: every release still +needs its own post-publish registry integrity, signature, and provenance verification. +Generated SBOM/provenance describes composition/origin; it does not prove safety. + +The tag workflow generates CycloneDX, audit, signature and license outputs before packing, +fails before publication when any gate fails, publishes the exact generated tarball, and then +compares registry integrity/shasum while requiring a signature and provenance. + +Dependencies and vendored runtimes must remain lockfile-pinned and receive license, +vulnerability, view-time network, CSP, browser-weight, update-owner, and removal-path review. +Permissive licenses are the default. Reciprocal, source-available, unknown, or conflicting +terms need explicit maintainer/legal review. Removing the package must not break already +created self-contained pages. diff --git a/docs/threat-model.md b/docs/threat-model.md new file mode 100644 index 0000000..dea6523 --- /dev/null +++ b/docs/threat-model.md @@ -0,0 +1,118 @@ +# Threat model + +Model version: 1. Last reviewed: 2026-08-16. Owner: Security maintainer. + +Review this model when a trust boundary, parser/runtime, storage schema, audience, deployment +provider, identity/grant model, dependency, CSP, or release authority changes, and after a +relevant incident. A boundary with missing controls/tests fails its capability claim. Planned +boundaries describe required future work and are never evidence of shipped protection. + +## Portable declarative pages + +- Assets/authority: authored Markdown/JSON, generated HTML and browser execution; renderer + code owns the trusted shell. +- Untrusted input/threats: text, component specs and chart data may inject markup/scripts, + escape a script context, request a network resource, exhaust rendering, or leak a secret. +- Controls/tests: Markdown raw HTML is escaped, user text uses escaping helpers, component + schemas reject malformed values, JSON/script boundaries are encoded, final size is capped, + credential patterns block publication, runtimes are inlined, and on-disk CSP has + `connect-src 'none'`; renderer/component/guard tests cover adversarial inputs. +- Residual risk: scanners and schema coverage are finite; complex vendored browser runtimes + retain upstream vulnerabilities and untrusted content can still reveal non-secret private + information if the author publishes it. + +## Trusted HTML mode + +- Assets/authority: explicit `format: "html"` input and the renderer shell; the author holds + code-execution authority for that page. +- Untrusted input/threats: arbitrary HTML/JavaScript can mislead viewers, read page-local data, + consume resources, or weaken declarative-renderer guarantees. +- Controls/tests: the mode is explicit, still receives the outer CSP/size/secret scan, and is + documented as outside fixed-renderer content safety. +- Residual risk: content is intentionally trusted and should never be accepted from an + untrusted author. Current tests do not sandbox it from same-page state. + +## Filesystem and local state + +- Assets/authority: artifact files, gallery manifest, versions, state/DB/datasource config and + user filesystem permission. +- Untrusted input/threats: slugs/paths may traverse, symlinks or concurrent processes may race, + partial writes may corrupt state, and private local files may enter a deploy tree. +- Controls/tests: bounded validated slugs, contained paths, atomic artifact writes, stale-hash + refusal, state directories excluded from deploy copies, and path/manifest/publisher tests. +- Residual risk: locking is process-local, lifecycle transactions are not fully crash-safe, + symlink/adversarial filesystem coverage is incomplete, and local deletion is multi-surface. + +## Loopback service + +- Assets/authority: local HTTP API, artifacts, comments/decisions/DB, registered datasource + commands and the user running the process. +- Untrusted input/threats: hostile local/web requests, traversal, oversized/malformed bodies, + cross-origin access, command misuse, denial of service, or exposure beyond the machine. +- Controls/tests: binds 127.0.0.1, validates routes/body shapes and datasource allowlist, + blocks traversal, keeps shell datasources out of hosted Worker, and tests malformed routes. +- Residual risk: no authentication, quota or multi-process CAS; any process/user able to reach + loopback may act with the server user's local authority. It is not a LAN/team service. + +## Deployment adapters + +- Assets/authority: destination name/repository, local artifact tree, `gh`/Wrangler authority + and provider account. +- Untrusted input/threats: target confusion, credential leakage, staging private state, + command/output spoofing, partial publish, or accidental audience expansion. +- Controls/tests: explicit target selection/config; scans of every current top-level public + file, provider target configuration, and reused destination/staging trees before network + mutation; fixed argument arrays; state-directory exclusion; scoped Worker KV names; and + staged-deploy unit tests. A force override applies only to that exact deploy invocation. +- Residual risk: provider CLIs/settings and account permissions are external; confirmation and + dry-run semantics are incomplete and provider rollback/deletion are not transactional. + +## Public static targets + +- Assets/authority: public artifact bytes, repository/history/CDN and user-controlled audience. +- Untrusted input/threats: permanent private-data disclosure, malicious content, lost + attribution, cache/fork persistence, abuse and ambiguous target visibility. +- Controls/tests: public target is explicit, content/frontmatter/title scanning precedes local + publication, the complete current public tree and provider target are rescanned before + deploy, GitHub state dirs are excluded, and policies disclose operator/removal limitations. +- Residual risk: scanners miss sensitive content, global deletion is impossible, and public + consumers can copy bytes. GitHub Pages has no mutable state channel. + +## Hosted content and control plane + +- Assets/authority: Cloudflare Worker/static assets, KV namespace, routes/config, user account. +- Untrusted input/threats: cross-worker state collision, exposed origin, provider compromise, + stale edge data, quota exhaustion, and state loss. +- Controls/tests: worker-scoped KV namespace naming, API validation, hosted datasource refusal, + and Worker handler/publisher tests. Availability is partial. +- Residual risk: KV is eventually consistent, writes lack CAS, quotas/backup/restore and origin + fail-closed behavior are not production-verified. The account owner is the operator. + +## Audience and identity + +- Assets/authority: future audience policy, viewer identity/roles, sessions and revocation. +- Untrusted input/threats: forged identity headers, stale access, confused deputy, privilege + escalation, public-origin bypass and audit gaps. +- Controls/tests: none shipped. Manual Cloudflare Access may front a user deployment but the + package does not configure, verify, consume or authorize identity. +- Residual risk: entire boundary is planned; authenticated collaboration claims are refused. + +## Mutable collaboration state + +- Assets/authority: decisions, comments, mini-DB documents, current artifact revision and KV/ + local filesystem writer. +- Untrusted input/threats: lost updates, stale writes, malformed documents, cross-artifact + access, replay, quota exhaustion and missing audit history. +- Controls/tests: bounded route identifiers and shapes, stale artifact publish guard, separated + local/Worker handlers, and state/DB tests. Availability is partial. +- Residual risk: no cross-process/hosted CAS, identity, audit, reconnect guarantee, quota model + or durable backup/restore gate. + +## Viewer-scoped connectors + +- Assets/authority: future viewer grants, provider credentials, query/results, cache and audit. +- Untrusted input/threats: cross-viewer data leak, overbroad grant, prompt/query injection, + retry side effects, stale cache, provider outage and credential exposure. +- Controls/tests: no hosted viewer connector exists; local datasources are fixed commands and + are refused by the Worker. +- Residual risk: entire boundary is planned; connector capability claims are refused. diff --git a/package-lock.json b/package-lock.json index a7c6de9..c4cd870 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,20 @@ { "name": "opencode-artifacts", - "version": "0.4.0", + "version": "0.14.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opencode-artifacts", - "version": "0.4.0", + "version": "0.14.4", "license": "MIT", "dependencies": { - "echarts": "^5.6.0", + "echarts": "^6.1.0", "markdown-it": "^14.1.0", "mermaid": "^11.16.1", - "vega": "^5.30.0", - "vega-embed": "^6.29.0", - "vega-lite": "^5.21.0" + "vega": "^6.4.0", + "vega-embed": "^7.1.0", + "vega-lite": "^6.4.3" }, "bin": { "opencode-artifacts": "dist/cli.js" @@ -26,7 +26,7 @@ "typescript": "^5.8.0" }, "engines": { - "node": ">=20" + "node": ">=24" }, "peerDependencies": { "@opencode-ai/plugin": ">=1.0.0" @@ -219,22 +219,6 @@ "cross-spawn": "7.0.6" } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", - "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -498,14 +482,12 @@ "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==", - "license": "MIT" + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==" }, "node_modules/@types/geojson": { - "version": "7946.0.4", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.4.tgz", - "integrity": "sha512-MHmwBtCb7OCv1DSivz2UNJXPGU/1btAWRKlqJ2saEhVJkpkvqHMMaOpKg0v4sAbDWSQekHGvPVMM8nQ+Jen03Q==", - "license": "MIT" + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==" }, "node_modules/@types/linkify-it": { "version": "5.0.0", @@ -560,24 +542,22 @@ } }, "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==", - "license": "MIT", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -590,37 +570,18 @@ "license": "Python-2.0" }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=12" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "node": ">=20" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -932,7 +893,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz", "integrity": "sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==", - "license": "ISC", "dependencies": { "commander": "7", "d3-array": "1 - 3", @@ -1210,13 +1170,12 @@ } }, "node_modules/echarts": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", - "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", - "license": "Apache-2.0", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz", + "integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==", "dependencies": { "tslib": "2.3.0", - "zrender": "5.6.1" + "zrender": "6.1.0" } }, "node_modules/effect": { @@ -1239,10 +1198,9 @@ } }, "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==" }, "node_modules/entities": { "version": "4.5.0", @@ -1271,7 +1229,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", "engines": { "node": ">=6" } @@ -1316,11 +1273,21 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/hachure-fill": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", @@ -1368,15 +1335,6 @@ "node": ">=12" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1579,26 +1537,6 @@ "dev": true, "license": "MIT" }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-gyp-build-optional-packages": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", @@ -1679,15 +1617,6 @@ ], "license": "MIT" }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/robust-predicates": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", @@ -1754,29 +1683,33 @@ } }, "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/stylis": { @@ -1808,7 +1741,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", - "license": "ISC", "dependencies": { "commander": "2" }, @@ -1821,14 +1753,7 @@ "node_modules/topojson-client/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "node_modules/ts-dedent": { "version": "2.3.0", @@ -1842,8 +1767,7 @@ "node_modules/tslib": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", - "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", - "license": "0BSD" + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" }, "node_modules/typescript": { "version": "5.9.3", @@ -1886,85 +1810,86 @@ } }, "node_modules/vega": { - "version": "5.33.1", - "resolved": "https://registry.npmjs.org/vega/-/vega-5.33.1.tgz", - "integrity": "sha512-1fArfVDUqVbb5z8n+/nVQb+VNBzx8svY6CrsyTK4Ujm4yrd9I1auoKxBAPXLCavY1LcrbHUskQbGUP8gXOzyQw==", - "license": "BSD-3-Clause", - "dependencies": { - "vega-crossfilter": "~4.1.4", - "vega-dataflow": "~5.7.8", - "vega-encode": "~4.10.3", - "vega-event-selector": "~3.0.1", - "vega-expression": "~5.2.1", - "vega-force": "~4.2.3", - "vega-format": "~1.1.4", - "vega-functions": "~5.18.1", - "vega-geo": "~4.4.4", - "vega-hierarchy": "~4.1.4", - "vega-label": "~1.3.2", - "vega-loader": "~4.5.4", - "vega-parser": "~6.6.1", - "vega-projection": "~1.6.3", - "vega-regression": "~1.3.2", - "vega-runtime": "~6.2.2", - "vega-scale": "~7.4.3", - "vega-scenegraph": "~4.13.2", - "vega-statistics": "~1.9.0", - "vega-time": "~2.1.4", - "vega-transforms": "~4.12.2", - "vega-typings": "~1.5.1", - "vega-util": "~1.17.4", - "vega-view": "~5.16.1", - "vega-view-transforms": "~4.6.2", - "vega-voronoi": "~4.2.5", - "vega-wordcloud": "~4.1.7" + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/vega/-/vega-6.4.0.tgz", + "integrity": "sha512-rHXCq2LpEWCLMjXBEj7hocY9G0/i5TGYRayGGSj06VTF4+l2IG2Z7oeZKCjWbUh/7hTE/mVhbaOPB72wCElh6g==", + "dependencies": { + "vega-crossfilter": "~5.1.3", + "vega-dataflow": "~6.1.3", + "vega-encode": "~5.2.2", + "vega-event-selector": "~4.0.0", + "vega-expression": "~6.2.2", + "vega-force": "~5.1.3", + "vega-format": "~2.1.3", + "vega-functions": "~6.2.0", + "vega-geo": "~5.1.3", + "vega-hierarchy": "~5.1.3", + "vega-label": "~2.1.3", + "vega-loader": "~5.1.3", + "vega-parser": "~7.1.3", + "vega-projection": "~2.1.3", + "vega-regression": "~2.1.3", + "vega-runtime": "~7.1.3", + "vega-scale": "~8.1.3", + "vega-scenegraph": "~5.3.0", + "vega-statistics": "~2.0.0", + "vega-time": "~3.3.0", + "vega-transforms": "~5.2.2", + "vega-typings": "~2.3.0", + "vega-util": "~2.1.3", + "vega-view": "~6.2.0", + "vega-view-transforms": "~5.2.2", + "vega-voronoi": "~5.1.3", + "vega-wordcloud": "~5.1.3" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" } }, "node_modules/vega-canvas": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/vega-canvas/-/vega-canvas-1.2.7.tgz", - "integrity": "sha512-OkJ9CACVcN9R5Pi9uF6MZBF06pO6qFpDYHWSKBJsdHP5o724KrsgR6UvbnXFH82FdsiTOff/HqjuaG8C7FL+9Q==", - "license": "BSD-3-Clause" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vega-canvas/-/vega-canvas-2.0.0.tgz", + "integrity": "sha512-9x+4TTw/USYST5nx4yN272sy9WcqSRjAR0tkQYZJ4cQIeon7uVsnohvoPQK1JZu7K1QXGUqzj08z0u/UegBVMA==" }, "node_modules/vega-crossfilter": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/vega-crossfilter/-/vega-crossfilter-4.1.4.tgz", - "integrity": "sha512-5E/i60Y80CUt+4O+U89X8ZnauaFuq0ztq/Hx4i4sT/crk4Lfn1oplRAjNOo7dFEZ04TahyBbYiJKIhR0yvmHpw==", - "license": "BSD-3-Clause", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/vega-crossfilter/-/vega-crossfilter-5.1.3.tgz", + "integrity": "sha512-goGulwrrbmv9mY4Za8HQiiFk7WV1OTyrSVZzrMR0Keeiyzh9cnkxRGF1W2fAhMNA3HSaRaBoxWHGr7H9fvovuQ==", "dependencies": { - "d3-array": "^3.2.2", - "vega-dataflow": "^5.7.8", - "vega-util": "^1.17.4" + "d3-array": "^3.2.4", + "vega-dataflow": "^6.1.3", + "vega-util": "^2.1.3" } }, "node_modules/vega-dataflow": { - "version": "5.7.8", - "resolved": "https://registry.npmjs.org/vega-dataflow/-/vega-dataflow-5.7.8.tgz", - "integrity": "sha512-jrllcIjSYU5Jh130RDR44o/SbUbJndLuoiM9IsKWW+a7HayKnfmbdHWm7MvCrj/YLupFZVojRaS1tTs53EXTdA==", - "license": "BSD-3-Clause", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/vega-dataflow/-/vega-dataflow-6.1.3.tgz", + "integrity": "sha512-ac51FLdYT8XAcDaHt3bQtVAR3UFdKfPMQBhjvBMXp69mdeq2ERfM1u9CUyDQAVZJylkIbWEwDnsp72tIwenPUA==", "dependencies": { - "vega-format": "^1.1.4", - "vega-loader": "^4.5.4", - "vega-util": "^1.17.4" + "vega-format": "^2.1.3", + "vega-loader": "^5.1.3", + "vega-util": "^2.1.3" } }, "node_modules/vega-embed": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/vega-embed/-/vega-embed-6.29.0.tgz", - "integrity": "sha512-PmlshTLtLFLgWtF/b23T1OwX53AugJ9RZ3qPE2c01VFAbgt3/GSNI/etzA/GzdrkceXFma+FDHNXUppKuM0U6Q==", - "license": "BSD-3-Clause", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/vega-embed/-/vega-embed-7.1.0.tgz", + "integrity": "sha512-ZmEIn5XJrQt7fSh2lwtSdXG/9uf3yIqZnvXFEwBJRppiBgrEWZcZbj6VK3xn8sNTFQ+sQDXW5sl/6kmbAW3s5A==", "dependencies": { "fast-json-patch": "^3.1.1", "json-stringify-pretty-compact": "^4.0.0", - "semver": "^7.6.3", + "semver": "^7.7.2", "tslib": "^2.8.1", - "vega-interpreter": "^1.0.5", - "vega-schema-url-parser": "^2.2.0", - "vega-themes": "^2.15.0", - "vega-tooltip": "^0.35.2" + "vega-interpreter": "^2.0.0", + "vega-schema-url-parser": "^3.0.2", + "vega-themes": "3.0.0", + "vega-tooltip": "1.0.0" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" }, "peerDependencies": { - "vega": "^5.21.0", + "vega": "*", "vega-lite": "*" } }, @@ -1975,137 +1900,127 @@ "license": "0BSD" }, "node_modules/vega-encode": { - "version": "4.10.3", - "resolved": "https://registry.npmjs.org/vega-encode/-/vega-encode-4.10.3.tgz", - "integrity": "sha512-245ebBuN1TjwVVDFG7JZaZ/ExLAhZ4kDTK2zlIoXs/g2P5rRgL4Q6oRixr+Pgr43G7ISCEHrUBgUjRcSoG8eEA==", - "license": "BSD-3-Clause", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/vega-encode/-/vega-encode-5.2.2.tgz", + "integrity": "sha512-YmriG349VjJsIxXE1gkxHj6fPF87T2t/fYmAUUlGzTGarSy3KF93t8Fqc6EPupsgnCDEhe3+0NsSWTVxiOJ1Ow==", "dependencies": { - "d3-array": "^3.2.2", + "d3-array": "^3.2.4", "d3-interpolate": "^3.0.1", - "vega-dataflow": "^5.7.8", - "vega-scale": "^7.4.3", - "vega-util": "^1.17.4" + "vega-dataflow": "^6.1.3", + "vega-scale": "^8.1.3", + "vega-util": "^2.1.3" } }, "node_modules/vega-event-selector": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/vega-event-selector/-/vega-event-selector-3.0.1.tgz", - "integrity": "sha512-K5zd7s5tjr1LiOOkjGpcVls8GsH/f2CWCrWcpKy74gTCp+llCdwz0Enqo013ZlGaRNjfgD/o1caJRt3GSaec4A==", - "license": "BSD-3-Clause" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vega-event-selector/-/vega-event-selector-4.0.0.tgz", + "integrity": "sha512-CcWF4m4KL/al1Oa5qSzZ5R776q8lRxCj3IafCHs5xipoEHrkgu1BWa7F/IH5HrDNXeIDnqOpSV1pFsAWRak4gQ==" }, "node_modules/vega-expression": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-5.2.1.tgz", - "integrity": "sha512-9KKbI2q9qTI55NSjD/dVWg3aeCtw+gwyWCiLMM47ha6iXrAN9pQ+EKRJfxOHuoDfCTlJJTaUfnnXgbqm0HEszg==", - "license": "BSD-3-Clause", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-6.2.2.tgz", + "integrity": "sha512-9yTpQBYDnl4yC27iGbUxDUnRAeO+la/lfzA5WSALB0INvA2A2NVxIorBakfqIkO7nwKM9bT3rmNRfj8Z3hjb7g==", "dependencies": { - "@types/estree": "^1.0.0", - "vega-util": "^1.17.4" + "@types/estree": "^1.0.9", + "vega-util": "^2.1.3" } }, "node_modules/vega-force": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/vega-force/-/vega-force-4.2.3.tgz", - "integrity": "sha512-7Yp1uOPy3eyzK/hnAIU0v/yQ9mIhtoJ+tq8AMaZiWqy67SUYsE3olmgdllY6R9M81D/n/rv8K+8JjbHMU5/sUA==", - "license": "BSD-3-Clause", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/vega-force/-/vega-force-5.1.3.tgz", + "integrity": "sha512-njBlnPeeMY0uz1Fbdqa5jnOsSUrUo2s3z/Eb4qo486hhrwNtNpmeCsN+4PBasoblf/4pOpbvKULzolZmG+R7QA==", "dependencies": { "d3-force": "^3.0.0", - "vega-dataflow": "^5.7.8", - "vega-util": "^1.17.4" + "vega-dataflow": "^6.1.3", + "vega-util": "^2.1.3" } }, "node_modules/vega-format": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/vega-format/-/vega-format-1.1.4.tgz", - "integrity": "sha512-+oz6UvXjQSbweW9P8q+1o2qFYyBYPFax94j6a9PQMnCIWMovFSss1wEElljOT8CEpnHyS15yiGlmz4qbWTQwnQ==", - "license": "BSD-3-Clause", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/vega-format/-/vega-format-2.1.3.tgz", + "integrity": "sha512-VK0yh3BtK5MUoG7aAXuwb678r2wDdykAUFIKHd9MWvXNo857UqNzZfYixfyXi8xXBK2AV38R4IiQmNKUrXBMUA==", "dependencies": { - "d3-array": "^3.2.2", - "d3-format": "^3.1.0", + "d3-array": "^3.2.4", + "d3-format": "^3.1.2", "d3-time-format": "^4.1.0", - "vega-time": "^2.1.4", - "vega-util": "^1.17.4" + "vega-time": "^3.3.0", + "vega-util": "^2.1.3" } }, "node_modules/vega-functions": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/vega-functions/-/vega-functions-5.18.1.tgz", - "integrity": "sha512-qEBAbo0jxGGebRvbX1zmxzmjwFz8/UtncRhzwk9/KcI0WudULNmCM1iTu+DGFRnNHdcKi6kUlwJBPIp7zDu3HQ==", - "license": "BSD-3-Clause", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/vega-functions/-/vega-functions-6.2.0.tgz", + "integrity": "sha512-MRFL7RjVmsv6iYuRZVvrDPZbySLa5Dzd5hC+0LTubysy8OrPengKZEHeRnibvHH6odg3IsKUV44Wv8dN3XzYAw==", "dependencies": { - "d3-array": "^3.2.2", + "d3-array": "^3.2.4", "d3-color": "^3.1.0", - "d3-geo": "^3.1.0", - "vega-dataflow": "^5.7.8", - "vega-expression": "^5.2.1", - "vega-scale": "^7.4.3", - "vega-scenegraph": "^4.13.2", - "vega-selections": "^5.6.1", - "vega-statistics": "^1.9.0", - "vega-time": "^2.1.4", - "vega-util": "^1.17.4" + "d3-ease": "^3.0.1", + "d3-geo": "^3.1.1", + "vega-dataflow": "^6.1.3", + "vega-expression": "^6.2.2", + "vega-scale": "^8.1.3", + "vega-scenegraph": "^5.3.0", + "vega-selections": "^6.1.5", + "vega-statistics": "^2.0.0", + "vega-time": "^3.3.0", + "vega-util": "^2.1.3" } }, "node_modules/vega-geo": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/vega-geo/-/vega-geo-4.4.4.tgz", - "integrity": "sha512-jTLYrDvhXJinWQCfuVLP1nSlOdFlA9blVS6K7uOcBTJ4382Aw3ALGZKluUQyJkFdrkGfqxoDJo5MLHLu2zlc6g==", - "license": "BSD-3-Clause", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/vega-geo/-/vega-geo-5.1.3.tgz", + "integrity": "sha512-UfTVPV+O+7elFyUsw8FLjiDOJkF0VZanC5tHLaSSue5h+xLzG6SDKQ7xEBIQSaTBE8nLSBtHm45Qve0SFjILVQ==", "dependencies": { - "d3-array": "^3.2.2", + "d3-array": "^3.2.4", "d3-color": "^3.1.0", - "d3-geo": "^3.1.0", - "vega-canvas": "^1.2.7", - "vega-dataflow": "^5.7.8", - "vega-projection": "^1.6.3", - "vega-statistics": "^1.9.0", - "vega-util": "^1.17.4" + "d3-geo": "^3.1.1", + "vega-canvas": "^2.0.0", + "vega-dataflow": "^6.1.3", + "vega-projection": "^2.1.3", + "vega-statistics": "^2.0.0", + "vega-util": "^2.1.3" } }, "node_modules/vega-hierarchy": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/vega-hierarchy/-/vega-hierarchy-4.1.4.tgz", - "integrity": "sha512-/iSh1YqdgsHFB20QxJ6IAVzRZQBKuMpZ/GsN5sZDP3bBJdVWl3we48L/r6w7FP9NU+JzFHcDUPJ0S0UzpMhaqg==", - "license": "BSD-3-Clause", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/vega-hierarchy/-/vega-hierarchy-5.1.3.tgz", + "integrity": "sha512-bDWDNGuUA4WcOD5CMh9Mahs3CwKxSU3W1XAnDGCpAwfJVoWp7vlVMt1paoTczPFTXtXGxIa3vJu94xWGmI3/og==", "dependencies": { "d3-hierarchy": "^3.1.2", - "vega-dataflow": "^5.7.8", - "vega-util": "^1.17.4" + "vega-dataflow": "^6.1.3", + "vega-util": "^2.1.3" } }, "node_modules/vega-interpreter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/vega-interpreter/-/vega-interpreter-1.2.1.tgz", - "integrity": "sha512-EMHLGxJ+SWfh1K/fHDRlHEZtLA/2ZNAXItYb5e8CxuAIm/Ha/3DHX/8VlvbTGIciUpuwmcKx4tVhJWlKreQ/Yw==", - "license": "BSD-3-Clause", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/vega-interpreter/-/vega-interpreter-2.3.2.tgz", + "integrity": "sha512-JDAoi3taFcCDLujZG84TNNUXdkAZ5WsSHssx8lWVYaxb9Slsjk7v7PtRIYXpSlUwLaKGRBqoJ9KDs36Z0eMEIw==", "dependencies": { - "vega-util": "^1.17.4" + "vega-util": "^2.1.3" } }, "node_modules/vega-label": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/vega-label/-/vega-label-1.3.2.tgz", - "integrity": "sha512-YlUCUZNsp1FHkpPjOpD+aVVmVLFw6xa+bFQGBCECuY1DuRyN6l8oCRmUOjBrr70ip8fbqfGk+czHw543J1PJgg==", - "license": "BSD-3-Clause", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/vega-label/-/vega-label-2.1.3.tgz", + "integrity": "sha512-UcaGgrVr2Gb0sUj4j3L5tZuL6GOn7tyauYsfXsS0hURoIbHCfQI4SnRq89vl5ub5DL1SEWaJTKag4GdAm1hcGw==", "dependencies": { - "vega-canvas": "^1.2.7", - "vega-dataflow": "^5.7.8", - "vega-scenegraph": "^4.13.2", - "vega-util": "^1.17.4" + "vega-canvas": "^2.0.0", + "vega-dataflow": "^6.1.3", + "vega-scenegraph": "^5.3.0", + "vega-util": "^2.1.3" } }, "node_modules/vega-lite": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-5.23.0.tgz", - "integrity": "sha512-l4J6+AWE3DIjvovEoHl2LdtCUkfm4zs8Xxx7INwZEAv+XVb6kR6vIN1gt3t2gN2gs/y4DYTs/RPoTeYAuEg6mA==", - "license": "BSD-3-Clause", + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-6.4.3.tgz", + "integrity": "sha512-d/7hPjfz560UERaQuTmGgIVfXAe3g2hJWeC+igDeaGohUdEoNrHLXgR/yTOBT8vV/lIuuKnw+0/xWWblkDwkMQ==", "dependencies": { "json-stringify-pretty-compact": "~4.0.0", "tslib": "~2.8.1", - "vega-event-selector": "~3.0.1", - "vega-expression": "~5.1.1", - "vega-util": "~1.17.2", - "yargs": "~17.7.2" + "vega-event-selector": "~4.0.0", + "vega-expression": "~6.1.0", + "vega-util": "~2.1.0", + "yargs": "~18.0.0" }, "bin": { "vl2pdf": "bin/vl2pdf", @@ -2114,10 +2029,13 @@ "vl2vg": "bin/vl2vg" }, "engines": { - "node": ">=18" + "node": ">=20" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" }, "peerDependencies": { - "vega": "^5.24.0" + "vega": "^6.0.0" } }, "node_modules/vega-lite/node_modules/tslib": { @@ -2127,257 +2045,222 @@ "license": "0BSD" }, "node_modules/vega-lite/node_modules/vega-expression": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-5.1.2.tgz", - "integrity": "sha512-fFeDTh4UtOxlZWL54jf1ZqJHinyerWq/ROiqrQxqLkNJRJ86RmxYTgXwt65UoZ/l4VUv9eAd2qoJeDEf610Umw==", - "license": "BSD-3-Clause", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-6.1.0.tgz", + "integrity": "sha512-hHgNx/fQ1Vn1u6vHSamH7lRMsOa/yQeHGGcWVmh8fZafLdwdhCM91kZD9p7+AleNpgwiwzfGogtpATFaMmDFYg==", "dependencies": { - "@types/estree": "^1.0.0", - "vega-util": "^1.17.3" + "@types/estree": "^1.0.8", + "vega-util": "^2.1.0" } }, "node_modules/vega-loader": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/vega-loader/-/vega-loader-4.5.4.tgz", - "integrity": "sha512-AOJPsDVz009aTdD9hzigUaO/NFmuN1o83rzvZu/g37TJfhU+3DOvgnO0rnqJbnSOfcBkLWER6XghlKS3j77w4A==", - "license": "BSD-3-Clause", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/vega-loader/-/vega-loader-5.1.3.tgz", + "integrity": "sha512-gJGoI262B5EEUGRFRY2mH+SJbVc+b4MTQfASNYpyIBqGxHqmjvNQqTyB+7bjReRYJjExjXL/MnKJmJHL6dAOwA==", "dependencies": { "d3-dsv": "^3.0.1", - "node-fetch": "^2.6.7", "topojson-client": "^3.1.0", - "vega-format": "^1.1.4", - "vega-util": "^1.17.4" + "vega-format": "^2.1.3", + "vega-util": "^2.1.3" } }, "node_modules/vega-parser": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/vega-parser/-/vega-parser-6.6.1.tgz", - "integrity": "sha512-FIez+huStzgjsxLqOkxDAooTkDC2XruYCIFWhd3HuM/QrqKtj9JKGuIUJjpVVkE7by7S5ejrucpRzVVgQfbzSg==", - "license": "BSD-3-Clause", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/vega-parser/-/vega-parser-7.1.3.tgz", + "integrity": "sha512-bT0pmzPF79ECFilKotgo92OUi25MGGgrEj9M0piiIpJZXQwcF2xsCf+5YRND1R0zd4zodNpqpJtIw0sl6TaqMg==", "dependencies": { - "vega-dataflow": "^5.7.8", - "vega-event-selector": "^3.0.1", - "vega-functions": "^5.18.1", - "vega-scale": "^7.4.3", - "vega-util": "^1.17.4" + "vega-dataflow": "^6.1.3", + "vega-event-selector": "^4.0.0", + "vega-functions": "^6.2.0", + "vega-scale": "^8.1.3", + "vega-util": "^2.1.3" } }, "node_modules/vega-projection": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/vega-projection/-/vega-projection-1.6.3.tgz", - "integrity": "sha512-vusyaPi3EFIHrBVs0chNv2bCqxw4X+XgI1m3+yOgUD/dutsIGTcqy+PjlNlspPQvMI9JjTeIUTGt8u1V7oDwAA==", - "license": "BSD-3-Clause", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/vega-projection/-/vega-projection-2.1.3.tgz", + "integrity": "sha512-IYGBnT+8a3ZH2bOM48c2qSZAnuPyyYTua/kMeXjwxFKI1/BjzlchQ9QCKxGeBqJGdG2pp9YMW2oS/F1dw/IQXw==", "dependencies": { - "d3-geo": "^3.1.0", + "d3-geo": "^3.1.1", "d3-geo-projection": "^4.0.0", - "vega-scale": "^7.4.3" + "vega-scale": "^8.1.3" } }, "node_modules/vega-regression": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/vega-regression/-/vega-regression-1.3.2.tgz", - "integrity": "sha512-DtGToopuJGmxeoymaOXTDKlWkkYiDVvZFV1IWQNuRlChTBI6YBpil4WmA3U+ECePDQuk2+/mWlw+vafrbgF8Tg==", - "license": "BSD-3-Clause", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/vega-regression/-/vega-regression-2.1.3.tgz", + "integrity": "sha512-qssCjc6KEV8pUQEEliZSTRv4cuhl5D2MgyfbKnIYbJM7B/lW/vubCPBr8oGzxVEGJd3D5kxEtXJORPipawdVEA==", "dependencies": { - "d3-array": "^3.2.2", - "vega-dataflow": "^5.7.8", - "vega-statistics": "^1.9.0", - "vega-util": "^1.17.4" + "d3-array": "^3.2.4", + "vega-dataflow": "^6.1.3", + "vega-statistics": "^2.0.0", + "vega-util": "^2.1.3" } }, "node_modules/vega-runtime": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/vega-runtime/-/vega-runtime-6.2.2.tgz", - "integrity": "sha512-5c+i7y5XBw5phIA1mBeqF/gtOQcDjUzroUAQ0g3y3weNx0K4mzj7V360yZiBLNcuHv65xgTlijstbKQttxeY/g==", - "license": "BSD-3-Clause", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/vega-runtime/-/vega-runtime-7.1.3.tgz", + "integrity": "sha512-27id9NGfnGh0u/NpQMagAmS5wDa7ELKDv1SYysiJp05HOYPuLD9XT3NSblPjZhDhyk1eApzWJAoegUoosJJtkA==", "dependencies": { - "vega-dataflow": "^5.7.8", - "vega-util": "^1.17.4" + "vega-dataflow": "^6.1.3", + "vega-util": "^2.1.3" } }, "node_modules/vega-scale": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/vega-scale/-/vega-scale-7.4.3.tgz", - "integrity": "sha512-f7SSN2YJowtrdkt7nJIR6YYhjDk8oB37q5So2/OxXQv5CBHipFPQSHS1ZVw9vD3V5wLnrZCxC4Ji27gmsTefgA==", - "license": "BSD-3-Clause", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vega-scale/-/vega-scale-8.1.3.tgz", + "integrity": "sha512-6Tx/1XMz2EtjOZm2zEONqJfYGfUWwSauhEyMCH5XWpnqoGjHQfDzDCFtBUFDs/6N+nNi3ldxCshraMiC4XCXLg==", "dependencies": { - "d3-array": "^3.2.2", + "d3-array": "^3.2.4", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-scale-chromatic": "^3.1.0", - "vega-time": "^2.1.4", - "vega-util": "^1.17.4" + "vega-time": "^3.3.0", + "vega-util": "^2.1.3" } }, "node_modules/vega-scenegraph": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/vega-scenegraph/-/vega-scenegraph-4.13.2.tgz", - "integrity": "sha512-eCutgcLzdUg23HLc6MTZ9pHCdH0hkqSmlbcoznspwT0ajjATk6M09JNyJddiaKR55HuQo03mBWsPeRCd5kOi0g==", - "license": "BSD-3-Clause", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/vega-scenegraph/-/vega-scenegraph-5.3.0.tgz", + "integrity": "sha512-sJbrDxGhyw8KFgC8NIEPBsZBZaOHAO4YtFcjw71bqIwVy6kIHlzc5I+buvJkEcBKkSTbsIdKRT1jUOHPDbBEKw==", "dependencies": { "d3-path": "^3.1.0", "d3-shape": "^3.2.0", - "vega-canvas": "^1.2.7", - "vega-loader": "^4.5.4", - "vega-scale": "^7.4.3", - "vega-util": "^1.17.4" + "vega-canvas": "^2.0.0", + "vega-loader": "^5.1.3", + "vega-scale": "^8.1.3", + "vega-util": "^2.1.3" } }, "node_modules/vega-schema-url-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vega-schema-url-parser/-/vega-schema-url-parser-2.2.0.tgz", - "integrity": "sha512-yAtdBnfYOhECv9YC70H2gEiqfIbVkq09aaE4y/9V/ovEFmH9gPKaEgzIZqgT7PSPQjKhsNkb6jk6XvSoboxOBw==", - "license": "BSD-3-Clause" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/vega-schema-url-parser/-/vega-schema-url-parser-3.0.2.tgz", + "integrity": "sha512-xAnR7KAvNPYewI3O0l5QGdT8Tv0+GCZQjqfP39cW/hbe/b3aYMAQ39vm8O2wfXUHzm04xTe7nolcsx8WQNVLRQ==" }, "node_modules/vega-selections": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/vega-selections/-/vega-selections-5.6.3.tgz", - "integrity": "sha512-DXd+XVKcIjBAtSCcgtPx7cXuqG/7L98SWoFh6GKNu26EBUyn3zm0GAlZxNLPoI01Jz9Fb3YpSsewk2aIAbM68g==", - "license": "BSD-3-Clause", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/vega-selections/-/vega-selections-6.1.5.tgz", + "integrity": "sha512-evYoCV1wuE0kuiDrKH2dVVweMUuz0pNN7rYTYKBSNDqa7TBjDU4+tbWFm/CgowoaAqjC6Omd+jEZz9lwpgeE3w==", "dependencies": { "d3-array": "3.2.4", - "vega-expression": "^5.2.1", - "vega-util": "^1.17.4" + "vega-expression": "^6.2.2", + "vega-util": "^2.1.3" } }, "node_modules/vega-statistics": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/vega-statistics/-/vega-statistics-1.9.0.tgz", - "integrity": "sha512-GAqS7mkatpXcMCQKWtFu1eMUKLUymjInU0O8kXshWaQrVWjPIO2lllZ1VNhdgE0qGj4oOIRRS11kzuijLshGXQ==", - "license": "BSD-3-Clause", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vega-statistics/-/vega-statistics-2.0.0.tgz", + "integrity": "sha512-dGPfDXnBlgXbZF3oxtkb8JfeRXd5TYHx25Z/tIoaa9jWua4Vf/AoW2wwh8J1qmMy8J03/29aowkp1yk4DOPazQ==", "dependencies": { - "d3-array": "^3.2.2" + "d3-array": "^3.2.4" } }, "node_modules/vega-themes": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/vega-themes/-/vega-themes-2.15.0.tgz", - "integrity": "sha512-DicRAKG9z+23A+rH/3w3QjJvKnlGhSbbUXGjBvYGseZ1lvj9KQ0BXZ2NS/+MKns59LNpFNHGi9us/wMlci4TOA==", - "license": "BSD-3-Clause", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vega-themes/-/vega-themes-3.0.0.tgz", + "integrity": "sha512-1iFiI3BNmW9FrsLnDLx0ZKEddsCitRY3XmUAwp6qmp+p+IXyJYc9pfjlVj9E6KXBPfm4cQyU++s0smKNiWzO4g==", + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + }, "peerDependencies": { "vega": "*", "vega-lite": "*" } }, "node_modules/vega-time": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/vega-time/-/vega-time-2.1.4.tgz", - "integrity": "sha512-DBMRps5myYnSAlvQ+oiX8CycJZjGQNqyGE04xaZrpOgHll7vlvezpET2FnGZC7wS3DsqMcPjnpnI1h7+qJox1Q==", - "license": "BSD-3-Clause", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/vega-time/-/vega-time-3.3.0.tgz", + "integrity": "sha512-bm9uMPrGIPQ52jD3Ltr6gUspogDtO0G8pEzLKvLySX84reeShHTH6jOd9YXwISNfuS4LT+cmPr9Ct6TMgvPMOA==", "dependencies": { - "d3-array": "^3.2.2", + "d3-array": "^3.2.4", "d3-time": "^3.1.0", - "vega-util": "^1.17.4" + "vega-util": "^2.1.3" } }, "node_modules/vega-tooltip": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/vega-tooltip/-/vega-tooltip-0.35.2.tgz", - "integrity": "sha512-kuYcsAAKYn39ye5wKf2fq1BAxVcjoz0alvKp/G+7BWfIb94J0PHmwrJ5+okGefeStZnbXxINZEOKo7INHaj9GA==", - "license": "BSD-3-Clause", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vega-tooltip/-/vega-tooltip-1.0.0.tgz", + "integrity": "sha512-P1R0JP29v0qnTuwzCQ0SPJlkjAzr6qeyj+H4VgUFSykHmHc1OBxda//XBaFDl/bZgIscEMvjKSjZpXd84x3aZQ==", "dependencies": { - "vega-util": "^1.17.2" + "vega-util": "^2.0.0" }, - "optionalDependencies": { - "@rollup/rollup-linux-x64-gnu": "^4.24.4" + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" } }, "node_modules/vega-transforms": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/vega-transforms/-/vega-transforms-4.12.2.tgz", - "integrity": "sha512-VuNLzB0DavFn5GIkFvR2XvwPavjY7VXl2oPUOFvNgSfyQywmCoC7VOX+iHeOdxoZdoy+XEOGAqRs9imxVo2HVA==", - "license": "BSD-3-Clause", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/vega-transforms/-/vega-transforms-5.2.2.tgz", + "integrity": "sha512-0vlu/36sBbHKw4dN/7BKCUPOzXc2oYObRxGMcROFLk+BwfjE/cqtfuXbLHzOinXtfLOhblmT0Agx+BSYXN8k/Q==", "dependencies": { - "d3-array": "^3.2.2", - "vega-dataflow": "^5.7.8", - "vega-statistics": "^1.9.0", - "vega-time": "^2.1.4", - "vega-util": "^1.17.4" + "d3-array": "^3.2.4", + "vega-dataflow": "^6.1.3", + "vega-statistics": "^2.0.0", + "vega-time": "^3.3.0", + "vega-util": "^2.1.3" } }, "node_modules/vega-typings": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/vega-typings/-/vega-typings-1.5.1.tgz", - "integrity": "sha512-40kRoG/jG8+4cd3kT5yw08iTlIGe57F7Go/ileSCtRtgU8RZ7tpPaE6GgYvF/HTPlCKMk9/pOdp62+o5sulhvQ==", - "license": "BSD-3-Clause", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/vega-typings/-/vega-typings-2.3.0.tgz", + "integrity": "sha512-CLS2Rd9MUlw1GLzXfNIrs6ubD0gMq/7AtaBUfhlmpAXjxnb15fZ3pJ9cOMmqu5lxdxHYr8eqSNoiIGgpJsiFMQ==", "dependencies": { - "@types/geojson": "7946.0.4", - "vega-event-selector": "^3.0.1", - "vega-expression": "^5.2.1", - "vega-util": "^1.17.4" + "@types/geojson": "7946.0.16", + "vega-event-selector": "^4.0.0", + "vega-expression": "^6.2.2", + "vega-util": "^2.1.3" } }, "node_modules/vega-util": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/vega-util/-/vega-util-1.17.4.tgz", - "integrity": "sha512-+y3ZW7dEqM8Ck+KRsd+jkMfxfE7MrQxUyIpNjkfhIpGEreym+aTn7XUw1DKXqclr8mqTQvbilPo16B3lnBr0wA==", - "license": "BSD-3-Clause" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/vega-util/-/vega-util-2.1.3.tgz", + "integrity": "sha512-Znj01Gj5XVUw/U7QvwjMhl+XCGs09UF5KDyMXZz9NtjhoKP04anWjcbA7hqoVH+eEigO92KlTi+Yai6Roo1n0A==" }, "node_modules/vega-view": { - "version": "5.16.1", - "resolved": "https://registry.npmjs.org/vega-view/-/vega-view-5.16.1.tgz", - "integrity": "sha512-YyG4i2JDkRCacHd9xNAwyov2cELxwzPGY224PA2o0gEhkoOjPkVElTmo9QHJvKeVxMoyad6wvoq+Jj+TB6zhLw==", - "license": "BSD-3-Clause", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/vega-view/-/vega-view-6.2.0.tgz", + "integrity": "sha512-e07jIm9BbLAnmSPCusxCNYstflMXE8qpQUKqfCmCcoRoE6TFnfGbjLLDsxeVpQtOnCj7PglxQw3Fx9/J4IEVaQ==", "dependencies": { - "d3-array": "^3.2.2", + "d3-array": "^3.2.4", "d3-timer": "^3.0.1", - "vega-dataflow": "^5.7.8", - "vega-format": "^1.1.4", - "vega-functions": "^5.18.1", - "vega-runtime": "^6.2.2", - "vega-scenegraph": "^4.13.2", - "vega-util": "^1.17.4" + "vega-dataflow": "^6.1.3", + "vega-format": "^2.1.3", + "vega-functions": "^6.2.0", + "vega-runtime": "^7.1.3", + "vega-scenegraph": "^5.3.0", + "vega-util": "^2.1.3" } }, "node_modules/vega-view-transforms": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/vega-view-transforms/-/vega-view-transforms-4.6.2.tgz", - "integrity": "sha512-udnYutgX7T7E0crqKWSs4hcxTdUmZHR2F4rKj0+3nN9+CfYMWbGgUkCkP2pMu7j2OH0ETX2uDn5xL8+YJWeXSg==", - "license": "BSD-3-Clause", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/vega-view-transforms/-/vega-view-transforms-5.2.2.tgz", + "integrity": "sha512-Zsfqy0AzCStVSEoS2lf416heWQp0lQpg9LxJhqLJVZLpTh9UzcTdxP5hMwN1StrB2gWCk7TEfBgJC9FBBzxiZA==", "dependencies": { - "vega-dataflow": "^5.7.8", - "vega-scenegraph": "^4.13.2", - "vega-util": "^1.17.4" + "vega-dataflow": "^6.1.3", + "vega-scenegraph": "^5.3.0", + "vega-util": "^2.1.3" } }, "node_modules/vega-voronoi": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/vega-voronoi/-/vega-voronoi-4.2.5.tgz", - "integrity": "sha512-u0TLSQboiLyoM6V9S0E6cc77EfWati+ApZ6zE+NhH3h/zZRzYZmElnDn46hUof2o9jNyh09xFXTXykVHmt7IGg==", - "license": "BSD-3-Clause", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/vega-voronoi/-/vega-voronoi-5.1.3.tgz", + "integrity": "sha512-1EYf2KFE/otZmjyQSw2xisnT6QDXouyofHBI9o9C1zOfNM284+wmJ0qbvL46wX9jEauH+RKG5qnx0eDWQHVjYw==", "dependencies": { - "d3-delaunay": "^6.0.2", - "vega-dataflow": "^5.7.8", - "vega-util": "^1.17.4" + "d3-delaunay": "^6.0.4", + "vega-dataflow": "^6.1.3", + "vega-util": "^2.1.3" } }, "node_modules/vega-wordcloud": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/vega-wordcloud/-/vega-wordcloud-4.1.7.tgz", - "integrity": "sha512-xdMykDXdWEp3/7Hil7Yx8uNVmjvqxwGibHJLSfiubNNO9OErrH3ZUgcxWv5hLe9wdK+KSGP94SSqTOAaqH7r/A==", - "license": "BSD-3-Clause", - "dependencies": { - "vega-canvas": "^1.2.7", - "vega-dataflow": "^5.7.8", - "vega-scale": "^7.4.3", - "vega-statistics": "^1.9.0", - "vega-util": "^1.17.4" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/vega-wordcloud/-/vega-wordcloud-5.1.3.tgz", + "integrity": "sha512-DxghqU1U9VPSKWmvatyQiDNShz1XfKv/XnIGlRWYdfTMnZvD3Y46mdrxPBhqFMF6TQKjZKyeqz5GtukA/RaH5Q==", "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "vega-canvas": "^2.0.0", + "vega-dataflow": "^6.1.3", + "vega-scale": "^8.1.3", + "vega-statistics": "^2.0.0", + "vega-util": "^2.1.3" } }, "node_modules/which": { @@ -2397,17 +2280,16 @@ } }, "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -2417,7 +2299,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", "engines": { "node": ">=10" } @@ -2439,30 +2320,27 @@ } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", - "license": "MIT", + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^7.2.0", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { @@ -2476,10 +2354,9 @@ } }, "node_modules/zrender": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", - "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", - "license": "BSD-3-Clause", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz", + "integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==", "dependencies": { "tslib": "2.3.0" } diff --git a/package.json b/package.json index 581a681..67beab5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opencode-artifacts", - "version": "0.14.3", + "version": "0.14.4", "description": "Publish OpenCode session output as self-contained, interactive HTML artifact pages.", "license": "MIT", "type": "module", @@ -38,17 +38,21 @@ "build": "tsc -p tsconfig.json", "test": "node --test test/*.test.ts", "check": "node scripts/check-repo.ts", + "check:links": "node scripts/check-links.ts", + "check:governance": "node scripts/governance-policy.ts", + "check:licenses": "node scripts/release-integrity.ts licenses package-lock.json docs/license-dispositions.json", + "study": "node scripts/journey-study.ts", "spec": "node scripts/spec-workflow.ts", "prepare": "git config core.hooksPath .githooks || true", "prepublishOnly": "npm run build" }, "dependencies": { - "echarts": "^5.6.0", + "echarts": "^6.1.0", "markdown-it": "^14.1.0", "mermaid": "^11.16.1", - "vega": "^5.30.0", - "vega-embed": "^6.29.0", - "vega-lite": "^5.21.0" + "vega": "^6.4.0", + "vega-embed": "^7.1.0", + "vega-lite": "^6.4.3" }, "peerDependencies": { "@opencode-ai/plugin": ">=1.0.0" @@ -60,6 +64,6 @@ "typescript": "^5.8.0" }, "engines": { - "node": ">=20" + "node": ">=24" } } diff --git a/scripts/check-links.ts b/scripts/check-links.ts new file mode 100644 index 0000000..8778650 --- /dev/null +++ b/scripts/check-links.ts @@ -0,0 +1,37 @@ +#!/usr/bin/env node + +import { resolve } from "node:path"; +import { + collectDocumentationLinks, + probeOfficialLinks, + validateLocalDocumentationLinks, +} from "./documentation-links.ts"; + +const root = resolve(import.meta.dirname, ".."); +const external = process.argv.slice(2).includes("--external"); + +const issues = await validateLocalDocumentationLinks(root); +for (const issue of issues) { + console.error( + `FAIL ${issue.sourcePath}:${issue.line} ${issue.target} [${issue.reason}] ${issue.detail}`, + ); +} +console.log(`${issues.length === 0 ? "ok" : "FAIL"} - local documentation links (${issues.length} issue(s))`); + +let externalFailures = 0; +if (external) { + const links = await collectDocumentationLinks(root); + const results = await probeOfficialLinks(links); + for (const result of results) { + console.log( + `${result.status === "pass" ? "ok" : "FAIL"} - ${result.url} [${result.status}] ${result.detail}` + + (result.finalUrl && result.finalUrl !== result.url ? ` -> ${result.finalUrl}` : ""), + ); + } + externalFailures = results.filter((result) => result.status !== "pass").length; + console.log(`${externalFailures === 0 ? "ok" : "FAIL"} - official source links (${externalFailures} failure(s))`); +} else { + console.log("skipped - official source links (run with --external)"); +} + +if (issues.length > 0 || externalFailures > 0) process.exitCode = 1; diff --git a/scripts/check-repo.ts b/scripts/check-repo.ts index 826ca9d..ef08300 100644 --- a/scripts/check-repo.ts +++ b/scripts/check-repo.ts @@ -3,7 +3,9 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; import { CHECKS, type Check } from "./checks.ts"; import { validateRequirementsTraceability } from "./requirements-traceability.ts"; +import { validateLocalDocumentationLinks } from "./documentation-links.ts"; import { validateSpecRepository } from "./spec-workflow-lib.ts"; +import { validateGovernanceRepository } from "./governance-policy.ts"; const root = join(import.meta.dirname, ".."); let failures = 0; @@ -74,6 +76,22 @@ async function evaluate(check: Check): Promise { report(errors.length === 0, check.id, errors.join("; ")); return; } + case "docs-links": { + const errors = await validateLocalDocumentationLinks(root); + report( + errors.length === 0, + check.id, + errors + .map((error) => `${error.sourcePath}:${error.line} ${error.target} [${error.reason}]`) + .join("; "), + ); + return; + } + case "governance-policy": { + const errors = await validateGovernanceRepository(root); + report(errors.length === 0, check.id, errors.join("; ")); + return; + } case "spec-workflow": { const errors = await validateSpecRepository(root); report(errors.length === 0, check.id, errors.join("; ")); diff --git a/scripts/checks.ts b/scripts/checks.ts index def2bc7..3224a46 100644 --- a/scripts/checks.ts +++ b/scripts/checks.ts @@ -6,6 +6,8 @@ export type Check = | { id: string; kind: "readme-one-liner" } | { id: string; kind: "readme-links" } | { id: string; kind: "requirements-traceability"; spec: string; traceability: string } + | { id: string; kind: "docs-links" } + | { id: string; kind: "governance-policy" } | { id: string; kind: "spec-workflow" } | { id: string; kind: "package-field"; field: "version-semver" | "metadata" | "files-skills" }; @@ -20,6 +22,8 @@ export const CHECKS: Check[] = [ { id: "readme-section-contributing", kind: "readme-section", section: "## Contributing" }, { id: "readme-section-license", kind: "readme-section", section: "## License" }, { id: "readme-links", kind: "readme-links" }, + { id: "docs-link-integrity", kind: "docs-links" }, + { id: "governance-policy", kind: "governance-policy" }, { id: "no-as-any", kind: "grep-forbidden", pattern: "as any", glob: "src/**/*.ts" }, { id: "no-ts-ignore", kind: "grep-forbidden", pattern: "@ts-ignore", glob: "src/**/*.ts" }, { id: "no-ts-expect-error", kind: "grep-forbidden", pattern: "@ts-expect-error", glob: "src/**/*.ts" }, diff --git a/scripts/documentation-links.ts b/scripts/documentation-links.ts new file mode 100644 index 0000000..0c3a1b2 --- /dev/null +++ b/scripts/documentation-links.ts @@ -0,0 +1,310 @@ +import { existsSync } from "node:fs"; +import { readFile, readdir } from "node:fs/promises"; +import { dirname, extname, relative, resolve, sep } from "node:path"; + +export type LinkKind = "local" | "official" | "external"; + +export interface DocumentationLink { + sourcePath: string; + line: number; + target: string; + kind: LinkKind; + rootRelative: boolean; +} + +export interface LinkIssue { + sourcePath: string; + line: number; + target: string; + reason: "path-escape" | "missing-path" | "missing-anchor" | "invalid-target"; + detail: string; +} + +export interface OfficialLinkResult { + url: string; + status: "pass" | "terminal-failure" | "transient-failure" | "timeout"; + httpStatus: number | null; + finalUrl: string | null; + detail: string; +} + +export type OfficialFetcher = ( + url: string, + signal: AbortSignal, +) => Promise<{ status: number; url: string }>; + +const OFFICIAL_HOSTS = [ + "anthropic.com", + "claude.com", + "code.claude.com", + "platform.claude.com", + "docs.github.com", + "docs.npmjs.com", + "developers.openai.com", + "learn.chatgpt.com", + "openai.com", + "opencode.ai", +]; + +const SCAN_ROOTS = ["README.md", "docs", "specs"]; +const EXCLUDED_PREFIXES = ["specs/templates/"]; + +function normalizePath(path: string): string { + return path.split(sep).join("/"); +} + +function isOfficialHost(hostname: string): boolean { + const lower = hostname.toLowerCase(); + return OFFICIAL_HOSTS.some((host) => lower === host || lower.endsWith(`.${host}`)); +} + +function withoutInlineCode(line: string): string { + let result = ""; + let index = 0; + while (index < line.length) { + const tick = line.indexOf("\`", index); + if (tick === -1) return result + line.slice(index); + result += line.slice(index, tick); + let width = 1; + while (line[tick + width] === "\`") width++; + const fence = "\`".repeat(width); + const end = line.indexOf(fence, tick + width); + if (end === -1) return result; + result += " ".repeat(end + width - tick); + index = end + width; + } + return result; +} + +function parseDestination(raw: string): string | undefined { + const value = raw.trim(); + if (value.startsWith("<")) { + const end = value.indexOf(">"); + return end === -1 ? undefined : value.slice(1, end); + } + const title = /\s+(?=["'])/.exec(value); + return (title ? value.slice(0, title.index) : value).trim() || undefined; +} + +export function extractDocumentationLinks( + markdown: string, + sourcePath: string, +): DocumentationLink[] { + const links: DocumentationLink[] = []; + const lines = markdown.split("\n"); + let fence: string | undefined; + + for (let index = 0; index < lines.length; index++) { + const line = lines[index]; + const fenceMatch = /^\s*([\`~]{3,})/.exec(line); + if (fenceMatch) { + const marker = fenceMatch[1][0]; + if (!fence) fence = marker; + else if (fence === marker) fence = undefined; + continue; + } + if (fence) continue; + + const visible = withoutInlineCode(line); + const linkPattern = /!?\[([^\]]*)\]\(([^)]+)\)/g; + for (const match of visible.matchAll(linkPattern)) { + const target = parseDestination(match[2]); + if (!target) continue; + const rootRelative = /^@(test|manual|model)$/.test(match[1]); + if (/^(?:mailto|data|javascript):/i.test(target)) continue; + if (/^https?:\/\//i.test(target)) { + try { + const url = new URL(target); + links.push({ + sourcePath, + line: index + 1, + target, + kind: isOfficialHost(url.hostname) ? "official" : "external", + rootRelative, + }); + } catch { + links.push({ sourcePath, line: index + 1, target, kind: "external", rootRelative }); + } + } else { + links.push({ sourcePath, line: index + 1, target, kind: "local", rootRelative }); + } + } + } + return links; +} + +function headingText(value: string): string { + return value + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") + .replace(/<[^>]*>/g, "") + .replace(/[\`*_~]/g, "") + .trim(); +} + +function githubSlug(value: string): string { + return headingText(value) + .toLocaleLowerCase("en-US") + .replace(/[^\p{L}\p{N}\s_-]/gu, "") + .replace(/\s/g, "-"); +} + +export function markdownAnchors(markdown: string): Set { + const anchors = new Set(); + const counts = new Map(); + let fence: string | undefined; + for (const line of markdown.split("\n")) { + const fenceMatch = /^\s*([\`~]{3,})/.exec(line); + if (fenceMatch) { + const marker = fenceMatch[1][0]; + if (!fence) fence = marker; + else if (fence === marker) fence = undefined; + continue; + } + if (fence) continue; + const heading = /^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/.exec(line); + if (!heading) continue; + const base = githubSlug(heading[1]); + const count = counts.get(base) ?? 0; + counts.set(base, count + 1); + anchors.add(count === 0 ? base : `${base}-${count}`); + } + return anchors; +} + +async function markdownPaths(root: string): Promise { + const paths: string[] = []; + for (const scanRoot of SCAN_ROOTS) { + const absolute = resolve(root, scanRoot); + if (!existsSync(absolute)) continue; + if (extname(absolute) === ".md") { + paths.push(scanRoot); + continue; + } + const entries = await readdir(absolute, { recursive: true, withFileTypes: true }); + for (const entry of entries) { + if (!entry.isFile() || !entry.name.endsWith(".md")) continue; + const path = normalizePath(relative(root, resolve(entry.parentPath, entry.name))); + if (!EXCLUDED_PREFIXES.some((prefix) => path.startsWith(prefix))) paths.push(path); + } + } + return paths.sort(); +} + +function safelyDecode(value: string): string | undefined { + try { + return decodeURIComponent(value); + } catch { + return undefined; + } +} + +function contained(root: string, target: string): boolean { + const path = relative(resolve(root), resolve(target)); + return path === "" || (!path.startsWith("..") && !path.startsWith(`..${sep}`)); +} + +export async function collectDocumentationLinks(root: string): Promise { + const links: DocumentationLink[] = []; + for (const path of await markdownPaths(root)) { + links.push(...extractDocumentationLinks(await readFile(resolve(root, path), "utf8"), path)); + } + return links; +} + +export async function validateLocalDocumentationLinks(root: string): Promise { + const issues: LinkIssue[] = []; + const anchorCache = new Map>(); + for (const link of await collectDocumentationLinks(root)) { + if (link.kind !== "local") continue; + const hash = link.target.indexOf("#"); + const query = link.target.indexOf("?"); + const boundary = [hash, query].filter((value) => value >= 0).sort((a, b) => a - b)[0]; + const rawPath = boundary === undefined ? link.target : link.target.slice(0, boundary); + const fragmentRaw = hash === -1 ? "" : link.target.slice(hash + 1).split("?")[0]; + const decodedPath = safelyDecode(rawPath); + const fragment = safelyDecode(fragmentRaw); + if (decodedPath === undefined || fragment === undefined) { + issues.push({ ...link, reason: "invalid-target", detail: "invalid percent encoding" }); + continue; + } + const absolute = decodedPath === "" + ? resolve(root, link.sourcePath) + : link.rootRelative + ? resolve(root, decodedPath) + : resolve(root, dirname(link.sourcePath), decodedPath); + if (!contained(root, absolute)) { + issues.push({ ...link, reason: "path-escape", detail: "target escapes the repository" }); + continue; + } + if (!existsSync(absolute)) { + issues.push({ ...link, reason: "missing-path", detail: normalizePath(relative(root, absolute)) }); + continue; + } + if (!fragment) continue; + if (extname(absolute).toLowerCase() !== ".md") { + issues.push({ ...link, reason: "missing-anchor", detail: "anchors are validated only for Markdown targets" }); + continue; + } + let anchors = anchorCache.get(absolute); + if (!anchors) { + anchors = markdownAnchors(await readFile(absolute, "utf8")); + anchorCache.set(absolute, anchors); + } + if (!anchors.has(fragment)) { + issues.push({ ...link, reason: "missing-anchor", detail: `heading #${fragment} does not exist` }); + } + } + return issues; +} + +async function defaultOfficialFetcher( + url: string, + signal: AbortSignal, +): Promise<{ status: number; url: string }> { + const response = await fetch(url, { + method: "GET", + redirect: "follow", + headers: { Range: "bytes=0-0", "User-Agent": "opencode-artifacts-link-check/1" }, + signal, + }); + await response.body?.cancel(); + return { status: response.status, url: response.url }; +} + +export async function probeOfficialLinks( + links: DocumentationLink[], + options: { timeoutMs?: number; fetcher?: OfficialFetcher } = {}, +): Promise { + const timeoutMs = options.timeoutMs ?? 10_000; + const fetcher = options.fetcher ?? defaultOfficialFetcher; + const urls = [...new Set(links.filter((link) => link.kind === "official").map((link) => link.target))].sort(); + const results: OfficialLinkResult[] = []; + for (const url of urls) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + const response = await fetcher(url, controller.signal); + const pass = response.status >= 200 && response.status < 400; + const transient = response.status === 408 || response.status === 429 || response.status >= 500; + results.push({ + url, + status: pass ? "pass" : transient ? "transient-failure" : "terminal-failure", + httpStatus: response.status, + finalUrl: response.url, + detail: pass ? "reachable" : `HTTP ${response.status}`, + }); + } catch (error) { + const timeout = controller.signal.aborted; + results.push({ + url, + status: timeout ? "timeout" : "transient-failure", + httpStatus: null, + finalUrl: null, + detail: timeout ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error), + }); + } finally { + clearTimeout(timer); + } + } + return results; +} diff --git a/scripts/governance-policy.ts b/scripts/governance-policy.ts new file mode 100644 index 0000000..71d46f2 --- /dev/null +++ b/scripts/governance-policy.ts @@ -0,0 +1,505 @@ +import { createHash } from "node:crypto"; +import { readdir, readFile } from "node:fs/promises"; +import { extname, join, relative, sep } from "node:path"; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function exactKeys( + value: Record, + required: string[], + path: string, + errors: string[], +): void { + for (const key of required) { + if (!(key in value)) errors.push(`${path} is missing ${key}`); + } + for (const key of Object.keys(value)) { + if (!required.includes(key)) errors.push(`${path} has unexpected field ${key}`); + } +} + +function textField(value: unknown, path: string, errors: string[]): value is string { + if (typeof value !== "string" || value.trim() === "") { + errors.push(`${path} must be a non-empty string`); + return false; + } + return true; +} + +const REQUIRED_SUPPORT_IDS = [ + "observed-linux-opencode", + "ubuntu-lts-desktop", + "macos-current-desktop", + "macos-previous-desktop", + "windows-11-native-desktop", + "windows-11-wsl-desktop", + "android-chrome-viewer", + "ios-safari-viewer", + "node-before-24", +]; + +const REQUIRED_MODES = [ + "portable-local", + "loopback-service", + "github-pages-public", + "cloudflare-public", + "authenticated-hosting", + "viewer-connectors", + "journey-study", + "release-evidence", +]; + +const REQUIRED_BOUNDARIES = [ + "portable-page", + "trusted-html", + "filesystem", + "loopback", + "deployment", + "public-static", + "hosted-content-control", + "audience-identity", + "mutable-state", + "connectors", +]; + +const REQUIRED_AUTHORED_SCOPES = [ + ".github/", + ".gitignore", + "AGENTS.md", + "LICENSE", + "README.md", + "SECURITY.md", + "agents/", + "docs/", + "examples/", + "package-lock.json", + "package.json", + "scripts/", + "skills/", + "src/", + "test/", + "tsconfig.json", +]; + +const REQUIRED_BENCHMARK_REFERENCES = [ + "https://code.claude.com/docs/en/artifacts", + "https://www.youtube.com/watch?v=m7TJqx8CYG8", +]; + +const ASSET_EXTENSIONS = new Set([ + ".avif", ".eot", ".gif", ".ico", ".jpeg", ".jpg", ".mp3", ".mp4", ".otf", + ".pdf", ".png", ".svg", ".ttf", ".wav", ".webm", ".webp", ".woff", ".woff2", +]); + +const FONT_EXTENSIONS = new Set([".eot", ".otf", ".ttf", ".woff", ".woff2"]); + +export function validateRedistributionInventory( + value: unknown, + actualAssets: Readonly>, +): string[] { + const errors: string[] = []; + if (!isRecord(value)) return ["redistribution inventory must be an object"]; + exactKeys( + value, + [ + "schemaVersion", + "reviewedAt", + "repositoryLicense", + "authoredScopes", + "binaryAssets", + "embeddedFonts", + "externalBenchmarkReferences", + "removedMaterials", + "dependencyEvidence", + ], + "redistribution", + errors, + ); + if (value["schemaVersion"] !== 1) errors.push("redistribution.schemaVersion must be 1"); + if (typeof value["reviewedAt"] !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value["reviewedAt"])) { + errors.push("redistribution.reviewedAt must be an ISO date"); + } + if (value["repositoryLicense"] !== "MIT") errors.push("redistribution.repositoryLicense must be MIT"); + + const scopes = value["authoredScopes"]; + const scopePaths = new Set(); + if (!Array.isArray(scopes)) { + errors.push("redistribution.authoredScopes must be an array"); + } else { + for (let index = 0; index < scopes.length; index++) { + const item = scopes[index]; + const path = `redistribution.authoredScopes[${index}]`; + if (!isRecord(item)) { + errors.push(`${path} must be an object`); + continue; + } + exactKeys(item, ["path", "provenance", "license"], path, errors); + if (textField(item["path"], `${path}.path`, errors)) { + if (scopePaths.has(item["path"])) errors.push(`${path}.path is duplicated`); + scopePaths.add(item["path"]); + } + if (item["provenance"] !== "repository-authored") errors.push(`${path}.provenance must be repository-authored`); + if (item["license"] !== "MIT") errors.push(`${path}.license must be MIT`); + } + for (const required of REQUIRED_AUTHORED_SCOPES) { + if (!scopePaths.has(required)) errors.push(`redistribution.authoredScopes is missing ${required}`); + } + } + + const assets = value["binaryAssets"]; + const inventoriedAssets = new Set(); + if (!Array.isArray(assets)) { + errors.push("redistribution.binaryAssets must be an array"); + } else { + for (let index = 0; index < assets.length; index++) { + const item = assets[index]; + const path = `redistribution.binaryAssets[${index}]`; + if (!isRecord(item)) { + errors.push(`${path} must be an object`); + continue; + } + exactKeys(item, ["path", "sha256", "provenance", "source", "license", "attribution", "status"], path, errors); + const assetPath = item["path"]; + if (textField(assetPath, `${path}.path`, errors)) { + if (inventoriedAssets.has(assetPath)) errors.push(`${path}.path is duplicated`); + inventoriedAssets.add(assetPath); + if (!(assetPath in actualAssets)) errors.push(`${path}.path is not a retained binary asset`); + if (actualAssets[assetPath] !== item["sha256"]) errors.push(`${path}.sha256 does not match retained bytes`); + } + if (typeof item["sha256"] !== "string" || !/^[a-f0-9]{64}$/.test(item["sha256"])) { + errors.push(`${path}.sha256 must be a lowercase SHA-256 digest`); + } + for (const field of ["provenance", "source", "attribution"]) { + textField(item[field], `${path}.${field}`, errors); + } + if (item["license"] !== "MIT") errors.push(`${path}.license must be MIT`); + if (item["status"] !== "approved") errors.push(`${path}.status must be approved`); + } + } + for (const assetPath of Object.keys(actualAssets)) { + if (!inventoriedAssets.has(assetPath)) errors.push(`redistribution.binaryAssets is missing ${assetPath}`); + } + + if (!Array.isArray(value["embeddedFonts"])) { + errors.push("redistribution.embeddedFonts must be an array"); + } else if (value["embeddedFonts"].length !== 0) { + errors.push("redistribution.embeddedFonts must remain empty until an exact font disposition is implemented"); + } + for (const assetPath of Object.keys(actualAssets)) { + if (FONT_EXTENSIONS.has(extname(assetPath).toLowerCase())) { + errors.push(`redistribution embeds an undisposed font: ${assetPath}`); + } + } + + const references = value["externalBenchmarkReferences"]; + const referenceUrls = new Set(); + if (!Array.isArray(references)) { + errors.push("redistribution.externalBenchmarkReferences must be an array"); + } else { + for (let index = 0; index < references.length; index++) { + const item = references[index]; + const path = `redistribution.externalBenchmarkReferences[${index}]`; + if (!isRecord(item)) { + errors.push(`${path} must be an object`); + continue; + } + exactKeys(item, ["url", "role", "localCopy", "redistribution"], path, errors); + if (textField(item["url"], `${path}.url`, errors)) referenceUrls.add(item["url"]); + textField(item["role"], `${path}.role`, errors); + if (item["localCopy"] !== null) errors.push(`${path}.localCopy must be null`); + if (item["redistribution"] !== "link-only") errors.push(`${path}.redistribution must be link-only`); + } + for (const url of REQUIRED_BENCHMARK_REFERENCES) { + if (!referenceUrls.has(url)) errors.push(`redistribution.externalBenchmarkReferences is missing ${url}`); + } + } + + const removed = value["removedMaterials"]; + if (!Array.isArray(removed)) { + errors.push("redistribution.removedMaterials must be an array"); + } else { + for (let index = 0; index < removed.length; index++) { + const item = removed[index]; + const path = `redistribution.removedMaterials[${index}]`; + if (!isRecord(item)) { + errors.push(`${path} must be an object`); + continue; + } + exactKeys(item, ["path", "sha256", "source", "reason", "status"], path, errors); + for (const field of ["path", "source", "reason"]) textField(item[field], `${path}.${field}`, errors); + if (typeof item["path"] === "string" && item["path"] in actualAssets) { + errors.push(`${path}.path is still retained`); + } + if (typeof item["sha256"] !== "string" || !/^[a-f0-9]{64}$/.test(item["sha256"])) { + errors.push(`${path}.sha256 must be a lowercase SHA-256 digest`); + } + if (item["status"] !== "removed") errors.push(`${path}.status must be removed`); + } + } + + const dependencyEvidence = value["dependencyEvidence"]; + if (!Array.isArray(dependencyEvidence) || dependencyEvidence.some((item) => typeof item !== "string" || item.trim() === "")) { + errors.push("redistribution.dependencyEvidence must be an array of non-empty paths"); + } else { + for (const required of ["package-lock.json", "docs/license-dispositions.json", "docs/evidence/governance/renderer-remediation-2026-08-16.md"]) { + if (!dependencyEvidence.includes(required)) errors.push(`redistribution.dependencyEvidence is missing ${required}`); + } + } + return errors; +} + +async function redistributedAssetHashes(root: string): Promise> { + const assets: Record = {}; + const ignored = new Set([".git", "dist", "node_modules"]); + async function walk(directory: string): Promise { + for (const entry of await readdir(directory, { withFileTypes: true })) { + if (entry.isDirectory() && ignored.has(entry.name)) continue; + const absolute = join(directory, entry.name); + if (entry.isDirectory()) { + await walk(absolute); + } else if (entry.isFile() && ASSET_EXTENSIONS.has(extname(entry.name).toLowerCase())) { + const path = relative(root, absolute).split(sep).join("/"); + assets[path] = createHash("sha256").update(await readFile(absolute)).digest("hex"); + } + } + } + await walk(root); + return assets; +} + +export function validateGovernancePolicy(value: unknown): string[] { + const errors: string[] = []; + if (!isRecord(value)) return ["policy must be an object"]; + exactKeys( + value, + ["schemaVersion", "policyVersion", "reviewedAt", "owners", "providerPrerequisites", "supportCells", "dataInventory", "threatBoundaries"], + "policy", + errors, + ); + if (value["schemaVersion"] !== 1) errors.push("policy.schemaVersion must be 1"); + if (value["policyVersion"] !== 1) errors.push("policy.policyVersion must be 1"); + if (typeof value["reviewedAt"] !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value["reviewedAt"])) { + errors.push("policy.reviewedAt must be an ISO date"); + } + + const owners = value["owners"]; + const ownerKeys = ["security", "privacy", "compatibility", "release"]; + if (!isRecord(owners)) { + errors.push("policy.owners must be an object"); + } else { + exactKeys(owners, ownerKeys, "policy.owners", errors); + for (const key of ownerKeys) textField(owners[key], `policy.owners.${key}`, errors); + } + + const prerequisites = value["providerPrerequisites"]; + if (!Array.isArray(prerequisites)) { + errors.push("policy.providerPrerequisites must be an array"); + } else { + const ids = new Set(); + for (let index = 0; index < prerequisites.length; index++) { + const item = prerequisites[index]; + const path = `policy.providerPrerequisites[${index}]`; + if (!isRecord(item)) { + errors.push(`${path} must be an object`); + continue; + } + exactKeys(item, ["id", "status", "checkedAt", "evidence", "claim"], path, errors); + if (textField(item["id"], `${path}.id`, errors)) { + if (ids.has(item["id"])) errors.push(`${path}.id is duplicated`); + ids.add(item["id"]); + } + if (!new Set(["pass", "failed", "unverified"]).has(String(item["status"]))) { + errors.push(`${path}.status is invalid`); + } + textField(item["checkedAt"], `${path}.checkedAt`, errors); + textField(item["evidence"], `${path}.evidence`, errors); + textField(item["claim"], `${path}.claim`, errors); + } + for (const id of ["github-private-vulnerability-reporting", "npm-trusted-publishing"]) { + if (!ids.has(id)) errors.push(`policy.providerPrerequisites is missing ${id}`); + } + } + + const cells = value["supportCells"]; + if (!Array.isArray(cells)) { + errors.push("policy.supportCells must be an array"); + } else { + const ids = new Set(); + for (let index = 0; index < cells.length; index++) { + const cell = cells[index]; + const path = `policy.supportCells[${index}]`; + if (!isRecord(cell)) { + errors.push(`${path} must be an object`); + continue; + } + exactKeys(cell, ["id", "class", "status", "os", "node", "opencode", "browser", "scope", "testedAt", "evidence"], path, errors); + if (textField(cell["id"], `${path}.id`, errors)) { + if (ids.has(cell["id"])) errors.push(`${path}.id is duplicated`); + ids.add(cell["id"]); + } + const status = String(cell["status"]); + if (!new Set(["target", "tested", "supported", "unsupported", "unverified"]).has(status)) { + errors.push(`${path}.status is invalid`); + } + if (!new Set(["target", "tested", "unsupported"]).has(String(cell["class"]))) { + errors.push(`${path}.class is invalid`); + } + for (const field of ["os", "node", "opencode", "browser", "scope"]) { + textField(cell[field], `${path}.${field}`, errors); + } + const hasDate = typeof cell["testedAt"] === "string" && cell["testedAt"] !== ""; + const hasEvidence = typeof cell["evidence"] === "string" && cell["evidence"] !== ""; + if ((status === "tested" || status === "supported") && (!hasDate || !hasEvidence)) { + errors.push(`${path} cannot be ${status} without dated evidence`); + } + if (status === "unverified" && (hasDate || hasEvidence)) { + errors.push(`${path} cannot be unverified with pass evidence`); + } + } + for (const id of REQUIRED_SUPPORT_IDS) { + if (!ids.has(id)) errors.push(`policy.supportCells is missing ${id}`); + } + } + + const inventory = value["dataInventory"]; + if (!Array.isArray(inventory)) { + errors.push("policy.dataInventory must be an array"); + } else { + const modes = new Set(); + const fields = ["mode", "availability", "fields", "purpose", "controllerOperator", "recipientLocation", "sensitivity", "retention", "deletion"]; + for (let index = 0; index < inventory.length; index++) { + const item = inventory[index]; + const path = `policy.dataInventory[${index}]`; + if (!isRecord(item)) { + errors.push(`${path} must be an object`); + continue; + } + exactKeys(item, fields, path, errors); + for (const field of fields) textField(item[field], `${path}.${field}`, errors); + if (typeof item["mode"] === "string") { + if (modes.has(item["mode"])) errors.push(`${path}.mode is duplicated`); + modes.add(item["mode"]); + } + if (!new Set(["current", "partial", "planned", "protocol-ready"]).has(String(item["availability"]))) { + errors.push(`${path}.availability is invalid`); + } + } + for (const mode of REQUIRED_MODES) { + if (!modes.has(mode)) errors.push(`policy.dataInventory is missing ${mode}`); + } + } + + const boundaries = value["threatBoundaries"]; + if (!Array.isArray(boundaries)) { + errors.push("policy.threatBoundaries must be an array"); + } else { + const ids = new Set(); + for (let index = 0; index < boundaries.length; index++) { + const item = boundaries[index]; + const path = `policy.threatBoundaries[${index}]`; + if (!isRecord(item)) { + errors.push(`${path} must be an object`); + continue; + } + exactKeys(item, ["id", "availability", "evidence"], path, errors); + if (textField(item["id"], `${path}.id`, errors)) ids.add(item["id"]); + if (!new Set(["current", "partial", "planned"]).has(String(item["availability"]))) { + errors.push(`${path}.availability is invalid`); + } + textField(item["evidence"], `${path}.evidence`, errors); + } + for (const id of REQUIRED_BOUNDARIES) { + if (!ids.has(id)) errors.push(`policy.threatBoundaries is missing ${id}`); + } + } + return errors; +} + +export interface GovernanceClaimInputs { + readme: string; + security: string; + support: string; + dataGovernance: string; + hosted: string; + packageJson: unknown; +} + +export function validateGovernanceClaims(inputs: GovernanceClaimInputs): string[] { + const errors: string[] = []; + if (inputs.readme.includes("with provenance attestations")) { + errors.push("README claims provenance attestations without registry evidence"); + } + for (const link of ["docs/support-policy.md", "docs/security.md", "docs/data-governance.md"]) { + if (!inputs.readme.includes(link)) errors.push(`README is missing governance link ${link}`); + } + if (!inputs.readme.includes("**Status: public preview.**") || + !inputs.readme.includes("unsupported") || !inputs.readme.includes("uncertified")) { + errors.push("README must expose unsupported and uncertified public-preview status"); + } + if (!inputs.security.includes("Private vulnerability reporting is currently **available**")) { + errors.push("SECURITY.md must expose verified private reporting"); + } + if (!inputs.support.includes("no fully supported platform/browser cells")) { + errors.push("support policy must expose the empty supported matrix"); + } + if (!inputs.support.includes("Public preview is explicitly unsupported and") || + !inputs.support.includes("uncertified")) { + errors.push("support policy must separate public preview from certification"); + } + if (!inputs.dataGovernance.includes("Local rendering sends no project usage telemetry")) { + errors.push("data policy must state the no-default-telemetry boundary"); + } + if (!inputs.hosted.includes("public by default")) { + errors.push("Cloudflare guide must expose public-by-default operation"); + } + if (!isRecord(inputs.packageJson) || !isRecord(inputs.packageJson["engines"]) || + inputs.packageJson["engines"]["node"] !== ">=24") { + errors.push("package engines.node must match the Node 24 support floor"); + } + return errors; +} + +export async function validateGovernanceRepository(root: string): Promise { + const read = (path: string): Promise => readFile(join(root, path), "utf8"); + try { + const [policyText, redistributionText, readme, security, support, dataGovernance, hosted, packageText, assets] = await Promise.all([ + read("docs/governance-policy.json"), + read("docs/redistribution-inventory.json"), + read("README.md"), + read("SECURITY.md"), + read("docs/support-policy.md"), + read("docs/data-governance.md"), + read("docs/hosted-cloudflare.md"), + read("package.json"), + redistributedAssetHashes(root), + ]); + const policy = JSON.parse(policyText) as unknown; + const redistribution = JSON.parse(redistributionText) as unknown; + return [ + ...validateGovernancePolicy(policy), + ...validateRedistributionInventory(redistribution, assets), + ...validateGovernanceClaims({ + readme, + security, + support, + dataGovernance, + hosted, + packageJson: JSON.parse(packageText) as unknown, + }), + ]; + } catch (error) { + return [`cannot load governance policy: ${error instanceof Error ? error.message : String(error)}`]; + } +} + +if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) { + const root = join(import.meta.dirname, ".."); + const errors = await validateGovernanceRepository(root); + if (errors.length > 0) { + for (const error of errors) console.error(`FAIL - ${error}`); + process.exit(1); + } + console.log("ok - governance policy and claims are consistent"); +} diff --git a/scripts/journey-study-lib.ts b/scripts/journey-study-lib.ts new file mode 100644 index 0000000..e437d58 --- /dev/null +++ b/scripts/journey-study-lib.ts @@ -0,0 +1,426 @@ +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function unexpectedKeys( + value: Record, + allowed: string[], + path: string, + errors: string[], +): void { + for (const key of Object.keys(value)) { + if (!allowed.includes(key)) errors.push(`${path} has unexpected field ${key}`); + } +} + +function nonEmptyString(value: unknown, path: string, errors: string[], max = 500): value is string { + if (typeof value !== "string" || value.trim() === "" || value.length > max) { + errors.push(`${path} must be a non-empty string of at most ${max} characters`); + return false; + } + return true; +} + +function timestamp(value: unknown, path: string, errors: string[]): value is string { + if (typeof value !== "string" || Number.isNaN(Date.parse(value))) { + errors.push(`${path} must be an ISO timestamp`); + return false; + } + return true; +} + +function boolean(value: unknown, path: string, errors: string[]): value is boolean { + if (typeof value !== "boolean") { + errors.push(`${path} must be boolean`); + return false; + } + return true; +} + +function boundedNumber( + value: unknown, + path: string, + errors: string[], + minimum: number, + maximum: number, +): value is number { + if (typeof value !== "number" || !Number.isFinite(value) || value < minimum || value > maximum) { + errors.push(`${path} must be between ${minimum} and ${maximum}`); + return false; + } + return true; +} + +export function validateJourneyCorpus(value: unknown): string[] { + const errors: string[] = []; + if (!isRecord(value)) return ["corpus must be an object"]; + unexpectedKeys(value, ["schemaVersion", "corpusVersion", "journeys", "fixtures"], "corpus", errors); + if (value["schemaVersion"] !== 1) errors.push("corpus.schemaVersion must be 1"); + if (value["corpusVersion"] !== 1) errors.push("corpus.corpusVersion must be 1"); + if (!Array.isArray(value["journeys"]) || value["journeys"].length < 4) { + errors.push("corpus.journeys must contain at least four journeys"); + } else { + const ids = new Set(); + const requiredStages = new Set(["create", "revise", "review", "share"]); + for (let index = 0; index < value["journeys"].length; index++) { + const journey = value["journeys"][index]; + const path = `corpus.journeys[${index}]`; + if (!isRecord(journey)) { + errors.push(`${path} must be an object`); + continue; + } + unexpectedKeys( + journey, + ["id", "stage", "status", "purpose", "preconditions", "success", "failure", "decisionState"], + path, + errors, + ); + if (nonEmptyString(journey["id"], `${path}.id`, errors, 80)) { + if (ids.has(journey["id"])) errors.push(`${path}.id is duplicated`); + ids.add(journey["id"]); + } + if (nonEmptyString(journey["stage"], `${path}.stage`, errors, 80)) { + requiredStages.delete(journey["stage"]); + } + for (const field of ["status", "purpose", "success", "failure"]) { + nonEmptyString(journey[field], `${path}.${field}`, errors); + } + for (const field of ["preconditions", "decisionState"]) { + const entries = journey[field]; + if (!Array.isArray(entries) || entries.length === 0) { + errors.push(`${path}.${field} must be a non-empty string array`); + } else { + for (let entry = 0; entry < entries.length; entry++) { + nonEmptyString(entries[entry], `${path}.${field}[${entry}]`, errors); + } + } + } + } + if (requiredStages.size > 0) { + errors.push(`corpus.journeys is missing required stages: ${[...requiredStages].join(", ")}`); + } + } + if (!Array.isArray(value["fixtures"]) || value["fixtures"].length === 0) { + errors.push("corpus.fixtures must be a non-empty array"); + } else { + const ids = new Set(); + for (let index = 0; index < value["fixtures"].length; index++) { + const fixture = value["fixtures"][index]; + const path = `corpus.fixtures[${index}]`; + if (!isRecord(fixture)) { + errors.push(`${path} must be an object`); + continue; + } + unexpectedKeys(fixture, ["id", "source", "rubric"], path, errors); + if (nonEmptyString(fixture["id"], `${path}.id`, errors, 80)) { + if (ids.has(fixture["id"])) errors.push(`${path}.id is duplicated`); + ids.add(fixture["id"]); + } + nonEmptyString(fixture["source"], `${path}.source`, errors, 300); + const rubric = fixture["rubric"]; + if (!isRecord(rubric)) { + errors.push(`${path}.rubric must be an object`); + } else { + unexpectedKeys(rubric, ["purpose", "primaryFinding", "provenance", "nextAction"], `${path}.rubric`, errors); + for (const field of ["purpose", "primaryFinding", "provenance", "nextAction"]) { + nonEmptyString(rubric[field], `${path}.rubric.${field}`, errors); + } + } + } + } + return errors; +} + +function fixtureIds(corpus: unknown): Set { + if (!isRecord(corpus) || !Array.isArray(corpus["fixtures"])) return new Set(); + return new Set( + corpus["fixtures"] + .filter(isRecord) + .map((fixture) => fixture["id"]) + .filter((id): id is string => typeof id === "string"), + ); +} + +function validateConsent( + value: unknown, + synthetic: boolean, + path: string, + errors: string[], +): void { + if (synthetic && value === null) return; + if (!isRecord(value)) { + errors.push(`${path} must be an object for real records and null only for synthetic records`); + return; + } + unexpectedKeys(value, ["given", "at", "protocolVersion", "withdrawnAt"], path, errors); + if (value["given"] !== true) errors.push(`${path}.given must be true`); + timestamp(value["at"], `${path}.at`, errors); + if (value["protocolVersion"] !== 1) errors.push(`${path}.protocolVersion must be 1`); + if (value["withdrawnAt"] !== null) timestamp(value["withdrawnAt"], `${path}.withdrawnAt`, errors); +} + +function validatePlatform(value: unknown, path: string, errors: string[]): void { + if (!isRecord(value)) { + errors.push(`${path} must be an object`); + return; + } + const fields = ["id", "os", "osVersion", "nodeVersion", "opencodeVersion", "browser", "browserVersion"]; + unexpectedKeys(value, fields, path, errors); + for (const field of fields) nonEmptyString(value[field], `${path}.${field}`, errors, 120); +} + +function validateFirstUse(value: unknown, path: string, errors: string[]): void { + if (value === null) return; + if (!isRecord(value)) { + errors.push(`${path} must be an object or null`); + return; + } + unexpectedKeys( + value, + ["startedAt", "endedAt", "elapsedSeconds", "completed", "readmeOnly", "repositoryCheckout", "hostingAccount", "maintainerAssistance", "failureStep"], + path, + errors, + ); + const startOk = timestamp(value["startedAt"], `${path}.startedAt`, errors); + const endOk = timestamp(value["endedAt"], `${path}.endedAt`, errors); + const elapsedOk = boundedNumber(value["elapsedSeconds"], `${path}.elapsedSeconds`, errors, 0, 3600); + for (const field of ["completed", "readmeOnly", "repositoryCheckout", "hostingAccount", "maintainerAssistance"]) { + boolean(value[field], `${path}.${field}`, errors); + } + if (value["failureStep"] !== null) nonEmptyString(value["failureStep"], `${path}.failureStep`, errors, 300); + if (startOk && endOk && elapsedOk) { + const observed = (Date.parse(value["endedAt"]) - Date.parse(value["startedAt"])) / 1000; + if (observed < 0 || Math.abs(observed - value["elapsedSeconds"]) > 2) { + errors.push(`${path}.elapsedSeconds must match the bounded timestamps within two seconds`); + } + } +} + +function validateComprehension( + value: unknown, + knownFixtures: Set, + path: string, + errors: string[], +): void { + if (!isRecord(value)) { + errors.push(`${path} must be an object`); + return; + } + unexpectedKeys( + value, + ["fixtureId", "artifactSha256", "startedAt", "elapsedSeconds", "maintainerAssistance", "answers", "scores"], + path, + errors, + ); + if (nonEmptyString(value["fixtureId"], `${path}.fixtureId`, errors, 80) && !knownFixtures.has(value["fixtureId"])) { + errors.push(`${path}.fixtureId is not in the approved corpus`); + } + if (typeof value["artifactSha256"] !== "string" || !/^[a-f0-9]{64}$/.test(value["artifactSha256"])) { + errors.push(`${path}.artifactSha256 must be 64 lowercase hexadecimal characters`); + } + timestamp(value["startedAt"], `${path}.startedAt`, errors); + boundedNumber(value["elapsedSeconds"], `${path}.elapsedSeconds`, errors, 0, 600); + boolean(value["maintainerAssistance"], `${path}.maintainerAssistance`, errors); + for (const group of ["answers", "scores"]) { + const detail = value[group]; + if (!isRecord(detail)) { + errors.push(`${path}.${group} must be an object`); + continue; + } + const fields = ["purpose", "primaryFinding", "provenance", "nextAction"]; + unexpectedKeys(detail, fields, `${path}.${group}`, errors); + for (const field of fields) { + if (group === "answers") nonEmptyString(detail[field], `${path}.${group}.${field}`, errors); + else boolean(detail[field], `${path}.${group}.${field}`, errors); + } + } +} + +export function validateJourneyStudy(value: unknown, corpus: unknown): string[] { + const errors = validateJourneyCorpus(corpus).map((error) => `invalid corpus: ${error}`); + if (!isRecord(value)) return [...errors, "study must be an object"]; + unexpectedKeys(value, ["schemaVersion", "studyId", "corpusVersion", "release", "claimedPlatformIds", "records"], "study", errors); + if (value["schemaVersion"] !== 1) errors.push("study.schemaVersion must be 1"); + nonEmptyString(value["studyId"], "study.studyId", errors, 120); + if (!isRecord(corpus) || value["corpusVersion"] !== corpus["corpusVersion"]) { + errors.push("study.corpusVersion must equal the approved corpus version"); + } + const release = value["release"]; + if (!isRecord(release)) { + errors.push("study.release must be an object"); + } else { + unexpectedKeys(release, ["package", "version", "integrity"], "study.release", errors); + if (release["package"] !== "opencode-artifacts") errors.push("study.release.package must be opencode-artifacts"); + nonEmptyString(release["version"], "study.release.version", errors, 80); + if (typeof release["integrity"] !== "string" || !/^(sha512-|sha256:)[A-Za-z0-9+/=:.-]+$/.test(release["integrity"])) { + errors.push("study.release.integrity must be a sha512 SRI or sha256 digest"); + } + } + const claimed = value["claimedPlatformIds"]; + if (!Array.isArray(claimed) || !claimed.every((id) => typeof id === "string" && id.trim() !== "")) { + errors.push("study.claimedPlatformIds must be a string array"); + } else if (new Set(claimed).size !== claimed.length) { + errors.push("study.claimedPlatformIds contains duplicates"); + } + if (!Array.isArray(value["records"])) { + errors.push("study.records must be an array"); + return errors; + } + if (value["records"].length > 100) errors.push("study.records cannot exceed 100 records"); + const ids = new Set(); + const knownFixtures = fixtureIds(corpus); + for (let index = 0; index < value["records"].length; index++) { + const record = value["records"][index]; + const path = `study.records[${index}]`; + if (!isRecord(record)) { + errors.push(`${path} must be an object`); + continue; + } + unexpectedKeys( + record, + ["participantId", "participantRole", "representative", "firstTimeUser", "conflict", "synthetic", "consent", "platform", "firstUse", "comprehension"], + path, + errors, + ); + const synthetic = record["synthetic"] === true; + if (!boolean(record["synthetic"], `${path}.synthetic`, errors)) continue; + if (nonEmptyString(record["participantId"], `${path}.participantId`, errors, 80)) { + const pattern = synthetic ? /^synthetic-[a-z0-9-]+$/ : /^p-[a-z0-9]{6,}$/; + if (!pattern.test(record["participantId"])) errors.push(`${path}.participantId is not pseudonymous for its record type`); + if (ids.has(record["participantId"])) errors.push(`${path}.participantId is duplicated`); + ids.add(record["participantId"]); + } + if (record["participantRole"] !== "primary" && record["participantRole"] !== "secondary") { + errors.push(`${path}.participantRole must be primary or secondary`); + } + boolean(record["representative"], `${path}.representative`, errors); + boolean(record["firstTimeUser"], `${path}.firstTimeUser`, errors); + if (!["none", "maintainer", "contributor", "rubric-reviewer", "other"].includes(String(record["conflict"]))) { + errors.push(`${path}.conflict is invalid`); + } + validateConsent(record["consent"], synthetic, `${path}.consent`, errors); + validatePlatform(record["platform"], `${path}.platform`, errors); + validateFirstUse(record["firstUse"], `${path}.firstUse`, errors); + validateComprehension(record["comprehension"], knownFixtures, `${path}.comprehension`, errors); + } + return errors; +} + +function realEligible(record: Record): boolean { + if (record["synthetic"] === true || record["participantRole"] !== "primary") return false; + if (record["representative"] !== true || record["conflict"] !== "none") return false; + const consent = record["consent"]; + return isRecord(consent) && consent["given"] === true && consent["withdrawnAt"] === null; +} + +function firstUsePass(record: Record): boolean { + const result = record["firstUse"]; + return record["firstTimeUser"] === true && isRecord(result) && + result["completed"] === true && result["readmeOnly"] === true && + result["repositoryCheckout"] === false && result["hostingAccount"] === false && + result["maintainerAssistance"] === false && typeof result["elapsedSeconds"] === "number" && + result["elapsedSeconds"] <= 600; +} + +function comprehensionPass(record: Record): boolean { + const result = record["comprehension"]; + if (!isRecord(result) || result["maintainerAssistance"] !== false || + typeof result["elapsedSeconds"] !== "number" || result["elapsedSeconds"] > 60) return false; + const scores = result["scores"]; + return isRecord(scores) && ["purpose", "primaryFinding", "provenance", "nextAction"] + .every((field) => scores[field] === true); +} + +export interface JourneyStudySummary { + studyId: string; + corpusVersion: number; + release: unknown; + totalRecords: number; + excluded: { synthetic: number; withdrawn: number; conflict: number; nonRepresentative: number; secondary: number }; + firstUse: { + status: "pass" | "fail" | "incomplete"; + claimedPlatforms: number; + coveredPlatforms: number; + eligibleRuns: number; + passingRuns: number; + failingRuns: number; + missingPlatformIds: string[]; + }; + comprehension: { + status: "pass" | "fail" | "incomplete"; + eligibleParticipants: number; + passingParticipants: number; + failingParticipants: number; + passRate: number | null; + threshold: number; + minimumParticipants: number; + fixtureDistribution: Record; + }; +} + +export function summarizeJourneyStudy(value: unknown, corpus: unknown): JourneyStudySummary { + const errors = validateJourneyStudy(value, corpus); + if (errors.length > 0) throw new Error(errors.join("\n")); + if (!isRecord(value) || !Array.isArray(value["records"]) || !Array.isArray(value["claimedPlatformIds"])) { + throw new Error("validated study shape was lost"); + } + const records = value["records"].filter(isRecord); + const eligible = records.filter(realEligible); + const claimedPlatforms = value["claimedPlatformIds"].filter((id): id is string => typeof id === "string"); + const firstUseRecords = eligible.filter((record) => record["firstTimeUser"] === true && record["firstUse"] !== null); + const coveredPlatforms = new Set(); + const passedPlatforms = new Set(); + for (const record of firstUseRecords) { + const platform = record["platform"]; + if (!isRecord(platform) || typeof platform["id"] !== "string") continue; + coveredPlatforms.add(platform["id"]); + if (firstUsePass(record)) passedPlatforms.add(platform["id"]); + } + const missingPlatformIds = claimedPlatforms.filter((id) => !passedPlatforms.has(id)); + const firstUseStatus = claimedPlatforms.length === 0 || claimedPlatforms.some((id) => !coveredPlatforms.has(id)) + ? "incomplete" + : missingPlatformIds.length > 0 ? "fail" : "pass"; + + const passing = eligible.filter(comprehensionPass); + const passRate = eligible.length === 0 ? null : passing.length / eligible.length; + const comprehensionStatus = eligible.length < 10 ? "incomplete" : passRate !== null && passRate >= 0.9 ? "pass" : "fail"; + const fixtureDistribution: Record = {}; + for (const record of eligible) { + const result = record["comprehension"]; + if (isRecord(result) && typeof result["fixtureId"] === "string") { + fixtureDistribution[result["fixtureId"]] = (fixtureDistribution[result["fixtureId"]] ?? 0) + 1; + } + } + return { + studyId: String(value["studyId"]), + corpusVersion: Number(value["corpusVersion"]), + release: value["release"], + totalRecords: records.length, + excluded: { + synthetic: records.filter((record) => record["synthetic"] === true).length, + withdrawn: records.filter((record) => isRecord(record["consent"]) && record["consent"]["withdrawnAt"] !== null).length, + conflict: records.filter((record) => record["conflict"] !== "none").length, + nonRepresentative: records.filter((record) => record["representative"] !== true).length, + secondary: records.filter((record) => record["participantRole"] === "secondary").length, + }, + firstUse: { + status: firstUseStatus, + claimedPlatforms: claimedPlatforms.length, + coveredPlatforms: claimedPlatforms.filter((id) => coveredPlatforms.has(id)).length, + eligibleRuns: firstUseRecords.length, + passingRuns: firstUseRecords.filter(firstUsePass).length, + failingRuns: firstUseRecords.filter((record) => !firstUsePass(record)).length, + missingPlatformIds, + }, + comprehension: { + status: comprehensionStatus, + eligibleParticipants: eligible.length, + passingParticipants: passing.length, + failingParticipants: eligible.length - passing.length, + passRate, + threshold: 0.9, + minimumParticipants: 10, + fixtureDistribution, + }, + }; +} diff --git a/scripts/journey-study.ts b/scripts/journey-study.ts new file mode 100644 index 0000000..5d265e0 --- /dev/null +++ b/scripts/journey-study.ts @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { summarizeJourneyStudy, validateJourneyStudy } from "./journey-study-lib.ts"; + +const root = resolve(import.meta.dirname, ".."); +const [command, input] = process.argv.slice(2); +if ((command !== "validate" && command !== "summarize") || !input) { + console.error("Usage: npm run study -- "); + process.exit(2); +} + +const corpus = JSON.parse(await readFile(resolve(root, "docs/journeys/corpus.json"), "utf8")) as unknown; +const study = JSON.parse(await readFile(resolve(process.cwd(), input), "utf8")) as unknown; +const errors = validateJourneyStudy(study, corpus); +if (errors.length > 0) { + for (const error of errors) console.error(`FAIL - ${error}`); + process.exit(1); +} + +if (command === "validate") { + console.log("ok - journey study records are structurally valid"); +} else { + console.log(JSON.stringify(summarizeJourneyStudy(study, corpus), null, 2)); +} diff --git a/scripts/release-integrity.ts b/scripts/release-integrity.ts new file mode 100644 index 0000000..f084dc0 --- /dev/null +++ b/scripts/release-integrity.ts @@ -0,0 +1,511 @@ +#!/usr/bin/env node + +import { createHash } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import { dirname, isAbsolute, resolve, sep } from "node:path"; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +export interface LicenseInventory { + packageEntries: number; + licenses: Record; + missingLicenseEntries: string[]; + reviewRequired: Array<{ path: string; license: string }>; +} + +export function createLicenseInventory( + value: unknown, + installedLicenses: Readonly> = {}, +): LicenseInventory { + if (!isRecord(value) || !isRecord(value["packages"])) { + throw new Error("lockfile.packages must be an object"); + } + const licenses: Record = {}; + const missingLicenseEntries: string[] = []; + const reviewRequired: Array<{ path: string; license: string }> = []; + let packageEntries = 0; + for (const [path, detail] of Object.entries(value["packages"])) { + if (path === "") continue; + packageEntries++; + const lockLicense = isRecord(detail) && typeof detail["license"] === "string" + ? detail["license"].trim() + : ""; + const installedLicense = installedLicenses[path]?.trim() ?? ""; + const license = lockLicense || installedLicense; + if (license === "") { + missingLicenseEntries.push(path); + licenses["MISSING"] = (licenses["MISSING"] ?? 0) + 1; + continue; + } + licenses[license] = (licenses[license] ?? 0) + 1; + if (/[()]|\b(?:AND|OR|WITH)\b/.test(license)) reviewRequired.push({ path, license }); + } + return { packageEntries, licenses, missingLicenseEntries, reviewRequired }; +} + +export function licenseInventoryErrors( + inventory: LicenseInventory, + approvedPaths: ReadonlySet = new Set(), +): string[] { + const errors: string[] = []; + const missing = inventory.missingLicenseEntries.filter((path) => !approvedPaths.has(path)); + if (missing.length > 0) { + errors.push(`missing license metadata: ${missing.join(", ")}`); + } + const reviewRequired = inventory.reviewRequired.filter((entry) => !approvedPaths.has(entry.path)); + if (reviewRequired.length > 0) { + errors.push( + `compound licenses need an explicit branch disposition: ${reviewRequired + .map((entry) => `${entry.path} (${entry.license})`) + .join(", ")}`, + ); + } + return errors; +} + +export interface LicenseDisposition { + path: string; + version: string; + declaredLicense: string | null; + selectedLicense: string; + licenseFile: string; + sha256: string; +} + +export interface LicenseDispositionValidation { + errors: string[]; + approvedPaths: Set; + entries: LicenseDisposition[]; +} + +const DISPOSITION_KEYS = [ + "path", + "version", + "declaredLicense", + "selectedLicense", + "licenseFile", + "sha256", +]; + +function exactKeys(value: Record, expected: string[], path: string, errors: string[]): void { + for (const key of expected) { + if (!(key in value)) errors.push(`${path} is missing ${key}`); + } + for (const key of Object.keys(value)) { + if (!expected.includes(key)) errors.push(`${path} has unexpected field ${key}`); + } +} + +function requiredText(value: unknown, path: string, errors: string[]): value is string { + if (typeof value !== "string" || value.trim() === "") { + errors.push(`${path} must be a non-empty string`); + return false; + } + return true; +} + +function packageEntries(value: unknown): Record | undefined { + return isRecord(value) && isRecord(value["packages"]) ? value["packages"] : undefined; +} + +function lockLicense(detail: unknown, installedLicenses: Readonly>, path: string): string | null { + if (isRecord(detail) && typeof detail["license"] === "string" && detail["license"].trim() !== "") { + return detail["license"].trim(); + } + const installed = installedLicenses[path]?.trim(); + return installed ? installed : null; +} + +export function validateLicenseDispositions( + lockfile: unknown, + installedLicenses: Readonly>, + value: unknown, + licenseFileHashes: Readonly>, +): LicenseDispositionValidation { + const errors: string[] = []; + const approvedPaths = new Set(); + const entries: LicenseDisposition[] = []; + const packages = packageEntries(lockfile); + if (!packages) return { errors: ["lockfile.packages must be an object"], approvedPaths, entries }; + if (!isRecord(value)) return { errors: ["license disposition document must be an object"], approvedPaths, entries }; + exactKeys(value, ["schemaVersion", "reviewedAt", "approvedBy", "approvedAt", "dispositions"], "dispositions", errors); + if (value["schemaVersion"] !== 1) errors.push("dispositions.schemaVersion must be 1"); + for (const field of ["reviewedAt", "approvedBy", "approvedAt"]) { + requiredText(value[field], `dispositions.${field}`, errors); + } + if (typeof value["approvedAt"] === "string" && Number.isNaN(Date.parse(value["approvedAt"]))) { + errors.push("dispositions.approvedAt must be a valid timestamp"); + } + if (!Array.isArray(value["dispositions"])) { + errors.push("dispositions.dispositions must be an array"); + return { errors, approvedPaths, entries }; + } + + const seen = new Set(); + for (let index = 0; index < value["dispositions"].length; index++) { + const item = value["dispositions"][index]; + const itemPath = `dispositions.dispositions[${index}]`; + if (!isRecord(item)) { + errors.push(`${itemPath} must be an object`); + continue; + } + exactKeys(item, DISPOSITION_KEYS, itemPath, errors); + const startErrors = errors.length; + const pathOk = requiredText(item["path"], `${itemPath}.path`, errors); + const versionOk = requiredText(item["version"], `${itemPath}.version`, errors); + const selectedOk = requiredText(item["selectedLicense"], `${itemPath}.selectedLicense`, errors); + const fileOk = requiredText(item["licenseFile"], `${itemPath}.licenseFile`, errors); + const hashOk = requiredText(item["sha256"], `${itemPath}.sha256`, errors); + const declared = item["declaredLicense"]; + if (declared !== null && (typeof declared !== "string" || declared.trim() === "")) { + errors.push(`${itemPath}.declaredLicense must be a non-empty string or null`); + } + if (!pathOk || !versionOk || !selectedOk || !fileOk || !hashOk || + (declared !== null && typeof declared !== "string")) continue; + const entry: LicenseDisposition = { + path: item["path"], + version: item["version"], + declaredLicense: declared, + selectedLicense: item["selectedLicense"], + licenseFile: item["licenseFile"], + sha256: item["sha256"], + }; + entries.push(entry); + if (seen.has(entry.path)) errors.push(`${itemPath}.path is duplicated`); + seen.add(entry.path); + const detail = packages[entry.path]; + if (!isRecord(detail)) { + errors.push(`${itemPath}.path is absent from the lockfile`); + } else { + if (detail["version"] !== entry.version) errors.push(`${itemPath}.version does not match the lockfile`); + const actualDeclared = lockLicense(detail, installedLicenses, entry.path); + if (actualDeclared !== entry.declaredLicense) errors.push(`${itemPath}.declaredLicense does not match installed metadata`); + if (actualDeclared !== null && !/[()]|\b(?:AND|OR|WITH)\b/.test(actualDeclared)) { + errors.push(`${itemPath} is not a missing or compound license disposition`); + } + if (actualDeclared !== null && !actualDeclared.includes(entry.selectedLicense)) { + errors.push(`${itemPath}.selectedLicense is not a branch of declaredLicense`); + } + } + const safeFile = !isAbsolute(entry.licenseFile) && !entry.licenseFile.split(/[\\/]/).includes("..") && + entry.licenseFile.startsWith(`${entry.path}/`); + if (!safeFile) errors.push(`${itemPath}.licenseFile must remain inside the disposed package`); + if (!/^[a-f0-9]{64}$/.test(entry.sha256)) errors.push(`${itemPath}.sha256 must be a lowercase SHA-256 digest`); + if (licenseFileHashes[entry.licenseFile] !== entry.sha256) errors.push(`${itemPath}.sha256 does not match the installed license file`); + if (errors.length === startErrors) approvedPaths.add(entry.path); + } + + return { errors, approvedPaths, entries }; +} + +export async function installedPackageLicenses(lockfile: unknown, root: string): Promise> { + const packages = packageEntries(lockfile); + if (!packages) throw new Error("lockfile.packages must be an object"); + const licenses: Record = {}; + await Promise.all(Object.keys(packages).filter((path) => path !== "").map(async (path) => { + try { + const manifest = JSON.parse(await readFile(resolve(root, path, "package.json"), "utf8")) as unknown; + if (isRecord(manifest) && typeof manifest["license"] === "string" && manifest["license"].trim() !== "") { + licenses[path] = manifest["license"].trim(); + } + } catch { + // Missing/unreadable installed metadata remains an explicit license finding. + } + })); + return licenses; +} + +async function licenseFileHashes(root: string, value: unknown): Promise> { + const hashes: Record = {}; + if (!isRecord(value) || !Array.isArray(value["dispositions"])) return hashes; + const rootPath = resolve(root); + await Promise.all(value["dispositions"].map(async (item) => { + if (!isRecord(item) || typeof item["licenseFile"] !== "string") return; + const file = item["licenseFile"]; + const absolute = resolve(rootPath, file); + if (isAbsolute(file) || (absolute !== rootPath && !absolute.startsWith(`${rootPath}${sep}`))) return; + try { + hashes[file] = createHash("sha256").update(await readFile(absolute)).digest("hex"); + } catch { + // A missing file is reported as a hash mismatch by disposition validation. + } + })); + return hashes; +} + +export interface ReleaseCandidateChecks { + audit: boolean; + licenses: boolean; + csp: boolean; + offline: boolean; + adversarialPayloads: boolean; + compatibility: boolean; + packedBytes: boolean; +} + +export function releaseCandidateGateFailures(checks: ReleaseCandidateChecks): string[] { + return (Object.entries(checks) as Array<[keyof ReleaseCandidateChecks, boolean]>) + .filter(([, passed]) => !passed) + .map(([name]) => name); +} + +export const PREVIEW_PREPUBLISH_GATES = [ + "tests", + "build", + "structural", + "package", + "finalSecretScan", + "csp", + "audit", + "licenses", + "redistribution", + "privateIntake", + "trustedPublishing", +] as const; + +export const PREVIEW_POSTPUBLISH_GATES = [ + "registryIntegrity", + "signature", + "provenance", +] as const; + +export type PreviewPrepublishGate = typeof PREVIEW_PREPUBLISH_GATES[number]; +export type PreviewPostpublishGate = typeof PREVIEW_POSTPUBLISH_GATES[number]; +export type ReleaseEvidenceStatus = "pass" | "failed" | "incomplete" | "unverified"; +export type ReleaseTransitionTarget = + | "development" + | "preview-candidate" + | "public-preview" + | "certified-local-core"; + +export interface ReleaseTransitionChecks { + hardGates: Record; + previewLabel: boolean; + unsupportedDisclosure: boolean; + missingEvidenceVisible: boolean; + certificationClaim: boolean; + out02: ReleaseEvidenceStatus; + out03: ReleaseEvidenceStatus; + support: ReleaseEvidenceStatus; +} + +function failedGates( + checks: ReleaseTransitionChecks, + gates: ReadonlyArray, +): string[] { + return gates.filter((gate) => !checks.hardGates[gate]).map((gate) => `hard gate failed: ${gate}`); +} + +function certificationEvidenceIsMissing(checks: ReleaseTransitionChecks): boolean { + return checks.out02 !== "pass" || checks.out03 !== "pass" || checks.support !== "pass"; +} + +export function releaseTransitionFailures( + target: ReleaseTransitionTarget, + checks: ReleaseTransitionChecks, +): string[] { + if (target === "development") return []; + + const failures = failedGates(checks, PREVIEW_PREPUBLISH_GATES); + if (target !== "preview-candidate") { + failures.push(...failedGates(checks, PREVIEW_POSTPUBLISH_GATES)); + } + + if (target === "preview-candidate" || target === "public-preview") { + if (!checks.previewLabel) failures.push("public preview label is missing"); + if (!checks.unsupportedDisclosure) failures.push("unsupported disclosure is missing"); + if (checks.certificationClaim) failures.push("public preview cannot claim certification"); + if (certificationEvidenceIsMissing(checks) && !checks.missingEvidenceVisible) { + failures.push("missing certification evidence is hidden"); + } + return failures; + } + + if (checks.previewLabel) failures.push("certified release cannot retain a preview label"); + if (checks.unsupportedDisclosure) failures.push("certified release cannot claim unsupported status"); + if (!checks.certificationClaim) failures.push("certified release must claim its exact certified level"); + for (const field of ["out02", "out03", "support"] as const) { + if (checks[field] !== "pass") failures.push(`certification evidence is not pass: ${field}`); + } + return failures; +} + +export interface PackCoordinate { + filename: string; + integrity: string; + shasum: string; + packageSpec: string; +} + +export function packCoordinate(pack: unknown, packageJson: unknown): PackCoordinate { + let result: Record | undefined; + if (Array.isArray(pack) && pack.length === 1 && isRecord(pack[0])) result = pack[0]; + else if (isRecord(pack)) { + const values = Object.values(pack); + if (values.length === 1 && isRecord(values[0])) result = values[0]; + } + if (!result) throw new Error("npm pack JSON must contain exactly one result"); + if (!isRecord(packageJson)) throw new Error("package.json must be an object"); + for (const field of ["filename", "integrity", "shasum"]) { + if (typeof result[field] !== "string" || result[field].trim() === "") { + throw new Error(`npm pack result is missing ${field}`); + } + } + if (typeof packageJson["name"] !== "string" || typeof packageJson["version"] !== "string") { + throw new Error("package.json must contain name and version"); + } + return { + filename: result["filename"] as string, + integrity: result["integrity"] as string, + shasum: result["shasum"] as string, + packageSpec: `${packageJson["name"]}@${packageJson["version"]}`, + }; +} + +export interface CandidateProvenanceEnvironment { + repository: string; + commit: string; + workflowRef: string; + runId: string; + runAttempt: string; + serverUrl: string; +} + +export function candidateProvenance( + pack: unknown, + packageJson: unknown, + sha256: string, + environment: CandidateProvenanceEnvironment, +): Record { + const coordinate = packCoordinate(pack, packageJson); + if (!/^[a-f0-9]{64}$/.test(sha256)) throw new Error("candidate SHA-256 digest is invalid"); + for (const [name, value] of Object.entries(environment)) { + if (value.trim() === "") throw new Error(`candidate provenance is missing ${name}`); + } + const repositoryUrl = `${environment.serverUrl}/${environment.repository}`; + const invocationUrl = `${repositoryUrl}/actions/runs/${environment.runId}/attempts/${environment.runAttempt}`; + return { + _type: "https://in-toto.io/Statement/v1", + subject: [{ name: coordinate.filename, digest: { sha256 } }], + predicateType: "https://slsa.dev/provenance/v1", + predicate: { + buildDefinition: { + buildType: "https://github.com/actions/runner", + externalParameters: { repository: repositoryUrl, commit: environment.commit }, + internalParameters: { workflowRef: environment.workflowRef }, + resolvedDependencies: [{ + uri: `git+${repositoryUrl}.git@${environment.commit}`, + digest: { gitCommit: environment.commit }, + }], + }, + runDetails: { + builder: { id: invocationUrl }, + metadata: { invocationId: invocationUrl }, + }, + }, + }; +} + +export function verifyPublishedDistribution(pack: unknown, dist: unknown): string[] { + const errors: string[] = []; + const registryDist = Array.isArray(dist) && dist.length === 1 && isRecord(dist[0]) + ? dist[0] + : dist; + if (!isRecord(pack) || !isRecord(registryDist)) { + return ["pack coordinate and registry dist must be objects or a singleton registry array"]; + } + for (const field of ["integrity", "shasum"]) { + if (typeof pack[field] !== "string" || registryDist[field] !== pack[field]) { + errors.push(`registry ${field} does not match the packed bytes`); + } + } + const attestations = registryDist["attestations"]; + if (!isRecord(attestations) || !isRecord(attestations["provenance"]) || + typeof attestations["provenance"]["predicateType"] !== "string") { + errors.push("registry provenance attestation is missing"); + } + if (!Array.isArray(registryDist["signatures"]) || registryDist["signatures"].length === 0) { + errors.push("registry package signature is missing"); + } + return errors; +} + +export function verifyTagVersion(packageJson: unknown, tag: string): string[] { + if (!isRecord(packageJson) || typeof packageJson["version"] !== "string") { + return ["package.json must contain a version"]; + } + const expected = `v${packageJson["version"]}`; + return tag === expected ? [] : [`release tag ${tag || ""} does not match package version ${expected}`]; +} + +async function json(path: string): Promise { + return JSON.parse(await readFile(path, "utf8")) as unknown; +} + +async function main(): Promise { + const [command, ...args] = process.argv.slice(2); + if (command === "licenses" && args.length === 2) { + const lockPath = resolve(args[0]); + const root = dirname(lockPath); + const lockfile = await json(lockPath); + const dispositionValue = await json(args[1]); + const installedLicenses = await installedPackageLicenses(lockfile, root); + const inventory = createLicenseInventory(lockfile, installedLicenses); + const validation = validateLicenseDispositions( + lockfile, + installedLicenses, + dispositionValue, + await licenseFileHashes(root, dispositionValue), + ); + console.log(JSON.stringify({ inventory, approvedDispositions: validation.entries }, null, 2)); + const errors = [...validation.errors, ...licenseInventoryErrors(inventory, validation.approvedPaths)]; + for (const error of errors) console.error(`FAIL - ${error}`); + if (errors.length > 0) process.exitCode = 1; + return; + } + if (command === "pack-output" && args.length === 2) { + const coordinate = packCoordinate(await json(args[0]), await json(args[1])); + console.log(`tarball=${coordinate.filename}`); + console.log(`package_spec=${coordinate.packageSpec}`); + console.log(`integrity=${coordinate.integrity}`); + console.log(`shasum=${coordinate.shasum}`); + return; + } + if (command === "candidate-provenance" && args.length === 2) { + const pack = await json(args[0]); + const packageJson = await json(args[1]); + const coordinate = packCoordinate(pack, packageJson); + const digest = createHash("sha256").update(await readFile(resolve(coordinate.filename))).digest("hex"); + const provenance = candidateProvenance(pack, packageJson, digest, { + repository: process.env["GITHUB_REPOSITORY"] ?? "", + commit: process.env["GITHUB_SHA"] ?? "", + workflowRef: process.env["GITHUB_WORKFLOW_REF"] ?? "", + runId: process.env["GITHUB_RUN_ID"] ?? "", + runAttempt: process.env["GITHUB_RUN_ATTEMPT"] ?? "", + serverUrl: process.env["GITHUB_SERVER_URL"] ?? "https://github.com", + }); + console.log(JSON.stringify(provenance, null, 2)); + return; + } + if (command === "verify-registry" && args.length === 3) { + const coordinate = packCoordinate(await json(args[0]), await json(args[1])); + const errors = verifyPublishedDistribution(coordinate, await json(args[2])); + for (const error of errors) console.error(`FAIL - ${error}`); + if (errors.length > 0) process.exitCode = 1; + else console.log("ok - registry integrity, signature, and provenance match the packed release"); + return; + } + if (command === "tag-version" && args.length === 2) { + const errors = verifyTagVersion(await json(args[0]), args[1]); + for (const error of errors) console.error(`FAIL - ${error}`); + if (errors.length > 0) process.exitCode = 1; + else console.log(`ok - release tag ${args[1]} matches package version`); + return; + } + console.error("Usage: release-integrity.ts licenses | pack-output | candidate-provenance | verify-registry | tag-version "); + process.exitCode = 2; +} + +if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) await main(); diff --git a/specs/archive/2026-08-16-contract-link-integrity/change.json b/specs/archive/2026-08-16-contract-link-integrity/change.json new file mode 100644 index 0000000..f49baf7 --- /dev/null +++ b/specs/archive/2026-08-16-contract-link-integrity/change.json @@ -0,0 +1,25 @@ +{ + "schemaVersion": 1, + "id": "contract-link-integrity", + "title": "Validate documentation links and anchors", + "lane": "standard", + "status": "archived", + "affectedRequirements": [ + "QUAL-08" + ], + "currentSpecs": [ + "specs/current/contract-integrity.spec.md" + ], + "currentSpecsUpdated": true, + "approval": { + "by": "bitgorust", + "at": "2026-08-16T08:17:19Z" + }, + "withdrawal": { + "by": "", + "at": "", + "reason": "" + }, + "createdAt": "2026-08-16", + "archivedAt": "2026-08-16" +} diff --git a/specs/archive/2026-08-16-contract-link-integrity/delta.md b/specs/archive/2026-08-16-contract-link-integrity/delta.md new file mode 100644 index 0000000..eb4d33b --- /dev/null +++ b/specs/archive/2026-08-16-contract-link-integrity/delta.md @@ -0,0 +1,29 @@ +# Specification delta: Validate documentation links and anchors + +## MODIFIED + +### Requirement: QUAL-08 + +The pre-release documentation check MUST scan Markdown contract surfaces for relative file +targets and heading anchors and MUST fail with source location and reason when one is invalid. +It MUST provide a separately invokable, bounded official-source URL check whose pass, fail, +and skipped results remain explicit; a skipped or transiently failed external check MUST NOT +be presented as a successful source validation. + +#### Scenario: Normal behavior + +- **Given:** contract Markdown links to existing files, valid headings, and reachable official sources +- **When:** maintainers run local and official-source link validation +- **Then:** every target is classified as passing and the report identifies the checked scope + +#### Scenario: Failure or refusal + +- **Given:** a Markdown link names a missing file, absent heading, or terminally unavailable official URL +- **When:** the applicable validator runs +- **Then:** it exits non-zero and reports the source file, line, target, and failure class without editing files + +#### Scenario: Relevant boundary + +- **Given:** external network validation is not requested or a request times out +- **When:** deterministic repository checks finish +- **Then:** local checks retain their result while external status is explicitly skipped or failed and is never recorded as pass diff --git a/specs/archive/2026-08-16-contract-link-integrity/evidence.md b/specs/archive/2026-08-16-contract-link-integrity/evidence.md new file mode 100644 index 0000000..8316ac6 --- /dev/null +++ b/specs/archive/2026-08-16-contract-link-integrity/evidence.md @@ -0,0 +1,12 @@ +# Evidence: Validate documentation links and anchors + +## Requirement: QUAL-08 + +- Validation: `bitgorust` approved the diagnostics, bounded network behavior, and visible + skipped/failure semantics on 2026-08-16. +- Verification: 99 repository tests passed, including four focused link tests; `npm run check` + passed with the registered local invariant; the live official-source run checked 27 unique + URLs with zero failures and retained redirects. +- Result: pass. +- Evidence: [@test](test/documentation-links.test.ts), + [@manual](docs/evidence/contract/official-links-2026-08-16.md) diff --git a/specs/archive/2026-08-16-contract-link-integrity/proposal.md b/specs/archive/2026-08-16-contract-link-integrity/proposal.md new file mode 100644 index 0000000..89f9a76 --- /dev/null +++ b/specs/archive/2026-08-16-contract-link-integrity/proposal.md @@ -0,0 +1,45 @@ +# Proposal: Validate documentation links and anchors + +## Outcome + +Repository validation detects broken local documentation targets, missing Markdown heading +anchors, and unavailable authoritative-source URLs before a contract or release claim is +accepted. + +## Context + +Phase 0 currently relies on prose links among the README, product contract, roadmap, +traceability matrix, component and hosting documentation, evidence, and official product +documentation. The existing `readme-links` assertion checks only whether a subset of README +paths exist; it does not validate anchors, links elsewhere in the contract, or official URLs. +A broken or redirected source can therefore leave capability language apparently supported +when its evidence path is not executable. + +## Scope + +- In scope: a dependency-free Markdown link extractor; deterministic validation of relative + file links and GitHub-style heading fragments under `README.md`, `docs/`, and `specs/`; + explicit classification of authoritative external URLs; a network-enabled official-link + command with bounded timeouts and visible failures; unit fixtures for encoding, duplicate + headings, non-file schemes, fragments, and ignored literal/code content; CI/structural + integration that cannot claim an external result when network checking was skipped. +- Out of scope: crawling arbitrary third-party sites, checking image pixel content, proving + that linked prose semantically supports a claim, rewriting links automatically, or treating + transient network failures as evidence that local contract structure is invalid. + +## Risks and rollback + +- Risk: Markdown parsing differences can create false positives; heading-slug emulation can + diverge from GitHub; external hosts can rate-limit or fail transiently; an unbounded crawler + could make deterministic checks slow or network-dependent. +- Rollback: keep local target/anchor validation as the deterministic repository check and + remove the network command/CI step if it proves unreliable. No artifact or user data is + migrated, and failure does not mutate documentation. + +## Validation plan + +Validate the proposal with the Phase 0 contract owners by showing that the command reports +the exact source path, line, target, and failure class and that skipped external checks remain +visible. Verify with table-driven tests containing good and broken local paths/anchors plus a +fake HTTP probe for success, redirect, timeout, and terminal failure. Retain one dated real +official-source run for Phase 0 rather than making live network state part of `npm test`. diff --git a/specs/archive/2026-08-16-contract-link-integrity/tasks.md b/specs/archive/2026-08-16-contract-link-integrity/tasks.md new file mode 100644 index 0000000..c5e4131 --- /dev/null +++ b/specs/archive/2026-08-16-contract-link-integrity/tasks.md @@ -0,0 +1,9 @@ +# Tasks: Validate documentation links and anchors + +- [x] Confirm proposal validation and human approval. +- [x] Add table-driven parser, local-target, anchor, and fake-network tests for every scenario. +- [x] Implement deterministic local validation and the bounded official-source command. +- [x] Register the local invariant in repository checks and CI; retain a dated real-source run. +- [x] Record validation and verification evidence. +- [x] Add `specs/current/contract-integrity.spec.md` and reconcile Phase 0 status. +- [x] Run repository validation and archive the packet. diff --git a/specs/archive/2026-08-16-journey-corpus-and-baseline/change.json b/specs/archive/2026-08-16-journey-corpus-and-baseline/change.json new file mode 100644 index 0000000..6275e74 --- /dev/null +++ b/specs/archive/2026-08-16-journey-corpus-and-baseline/change.json @@ -0,0 +1,28 @@ +{ + "schemaVersion": 1, + "id": "journey-corpus-and-baseline", + "title": "Establish the journey corpus and baseline study", + "lane": "standard", + "status": "archived", + "affectedRequirements": [ + "OUT-02", + "OUT-03", + "OUT-05", + "UX-01" + ], + "currentSpecs": [ + "specs/current/journey-evidence.spec.md" + ], + "currentSpecsUpdated": true, + "approval": { + "by": "bitgorust", + "at": "2026-08-16T08:17:19Z" + }, + "withdrawal": { + "by": "", + "at": "", + "reason": "" + }, + "createdAt": "2026-08-16", + "archivedAt": "2026-08-16" +} diff --git a/specs/archive/2026-08-16-journey-corpus-and-baseline/delta.md b/specs/archive/2026-08-16-journey-corpus-and-baseline/delta.md new file mode 100644 index 0000000..4fced8a --- /dev/null +++ b/specs/archive/2026-08-16-journey-corpus-and-baseline/delta.md @@ -0,0 +1,106 @@ +# Specification delta: Establish the journey corpus and baseline study + +## MODIFIED + +### Requirement: OUT-02 + +The Phase 0 first-use baseline MUST execute the released-package install, offline artifact +creation, and reopen journey from only README instructions on each claimed supported clean +machine. It MUST retain package identity, platform, bounded start/end timestamps, outcome, +failure point, and participant consent under a dated study identifier. Only an eligible +first-time participant completing the entire journey within ten minutes counts as a pass. + +#### Scenario: Normal behavior + +- **Given:** a consented eligible first-time user, claimed clean platform, and exact released package +- **When:** the user follows only the README to install, create, and reopen an offline artifact +- **Then:** the complete journey and elapsed time are retained and a time of at most ten minutes is scored pass + +#### Scenario: Failure or refusal + +- **Given:** the journey needs maintainer assistance, a repository checkout, hosting account, or exceeds ten minutes +- **When:** the result is scored +- **Then:** it is retained as a failure with the first failing step and is not omitted from the baseline + +#### Scenario: Relevant boundary + +- **Given:** a platform or package version is not identified or the participant is ineligible +- **When:** validation runs +- **Then:** the record is rejected or explicitly excluded with reason and cannot support a platform claim + +### Requirement: OUT-03 + +The comprehension baseline MUST use the checked-in corpus with at least ten consented, +eligible representative primary users. Without maintainer assistance, each participant gets +one minute to identify purpose, primary finding/state, provenance, and next action. A pass +requires all four; Phase 0 passes only when at least 90% pass, with every result, exclusion, +non-response, and fixture assignment visible. + +#### Scenario: Normal behavior + +- **Given:** at least ten eligible primary-user records from the approved corpus +- **When:** all four answers and elapsed time are scored by the approved rubric +- **Then:** the report shows the denominator, every outcome, and pass only when at least 90% identify all four facts within one minute + +#### Scenario: Failure or refusal + +- **Given:** fewer than 90% identify all four facts, a run needs maintainer assistance, or a response exceeds one minute +- **When:** the baseline is aggregated +- **Then:** the requirement and Phase 0 remain failed with the unsuccessful outcome visible + +#### Scenario: Relevant boundary + +- **Given:** fewer than ten eligible participants or one required fact has no scored answer +- **When:** aggregation is requested +- **Then:** the baseline is reported incomplete and cannot round, impute, or relabel missing data as pass + +### Requirement: OUT-05 + +Journey measurement MUST be voluntary and purpose-bound. Each retained record MUST reference +explicit consent, permit withdrawal, exclude direct identity and private artifact content, +and remain separate from product functionality. Synthetic harness fixtures MUST be labeled +test-only and MUST NOT contribute to the product baseline. + +#### Scenario: Normal behavior + +- **Given:** an eligible participant receives the purpose, collected fields, retention, and withdrawal terms +- **When:** they explicitly consent and complete a study task +- **Then:** a pseudonymous record is retained for that study purpose without changing product capability + +#### Scenario: Failure or refusal + +- **Given:** a person declines or withdraws consent +- **When:** study collection or withdrawal processing occurs +- **Then:** no new record is collected or the covered raw record is deleted while product use remains unchanged + +#### Scenario: Relevant boundary + +- **Given:** synthetic records exercise validator and aggregation behavior +- **When:** reports are generated +- **Then:** test-only records are unmistakably excluded from participant counts and acceptance results + +### Requirement: UX-01 + +The checked-in corpus MUST define create, revise, review, and share journeys with purpose, +preconditions, fixture, primary path, observable success, failure prompts, and the artifact +identity/revision/visibility/capability facts shown at each decision. Later reconnect, export, +archive, and restore journeys MAY be added by their owning phases but missing later-phase +behavior MUST remain visible. + +#### Scenario: Normal behavior + +- **Given:** an approved corpus version and a capability that currently ships +- **When:** a participant follows a create, revise, review, or share task +- **Then:** the task names its artifact state and has an observable, consistently scored endpoint + +#### Scenario: Failure or refusal + +- **Given:** a task depends on behavior that is partial, missing, or unavailable on the platform +- **When:** the task is selected +- **Then:** the protocol records that state and does not supply coaching or silently substitute another workflow + +#### Scenario: Relevant boundary + +- **Given:** a journey belongs to a later phase such as restore or authenticated sharing +- **When:** Phase 0 corpus completeness is reported +- **Then:** its deferred status is visible and Phase 0 does not claim that later behavior ships diff --git a/specs/archive/2026-08-16-journey-corpus-and-baseline/evidence.md b/specs/archive/2026-08-16-journey-corpus-and-baseline/evidence.md new file mode 100644 index 0000000..2c54903 --- /dev/null +++ b/specs/archive/2026-08-16-journey-corpus-and-baseline/evidence.md @@ -0,0 +1,41 @@ +# Evidence: Establish the journey corpus and baseline study + +## Requirement: OUT-02 + +- Validation: missing; no consented first-time-user run has been conducted. +- Verification: the approved corpus and validator are checked in; focused tests prove that no + platform, uncovered platforms, and fewer-than-required evidence stay incomplete, while an + observed covered miss fails. +- Result: incomplete, which is not a pass and cannot support certification; the approved + public-preview contract permits distribution only with this absence visible. +- Evidence: [@test](test/journey-study.test.ts), + [@manual](docs/evidence/journeys/phase-0-baseline-status.md) + +## Requirement: OUT-03 + +- Validation: missing; there are no results from ten consented representative primary users. +- Verification: focused tests exercise the exact ten-participant and 90% boundary, exclusion + rules, fixture distribution, and the empty-study result. +- Result: incomplete, which is not a pass and cannot support certification; the approved + public-preview contract permits distribution only with this absence visible. +- Evidence: [@test](test/journey-study.test.ts), + [@manual](docs/evidence/journeys/phase-0-baseline-status.md) + +## Requirement: OUT-05 + +- Validation: `bitgorust` approved the informed-consent, withdrawal, minimization, access, and + retention protocol on 2026-08-16. +- Verification: validation rejects absent consent and unexpected identity fields; aggregation + excludes withdrawn records and emits neither participant codes nor answer text. +- Result: pass for the Phase 0 study mechanism; no real participant data has been collected. +- Evidence: [@test](test/journey-study.test.ts), [@manual](docs/journeys/README.md) + +## Requirement: UX-01 + +- Validation: participant use is missing; reconnect/export/archive/restore remain explicitly + deferred rather than represented as shipped. +- Verification: the corpus validator requires create, revise, review, and share stages plus + complete decision-state and fixture-rubric fields. +- Result: partial; the Phase 0 corpus is protocol-ready but real use and later lifecycle paths + remain open. +- Evidence: [@test](test/journey-study.test.ts), [@manual](docs/journeys/corpus.json) diff --git a/specs/archive/2026-08-16-journey-corpus-and-baseline/proposal.md b/specs/archive/2026-08-16-journey-corpus-and-baseline/proposal.md new file mode 100644 index 0000000..eb23a88 --- /dev/null +++ b/specs/archive/2026-08-16-journey-corpus-and-baseline/proposal.md @@ -0,0 +1,48 @@ +# Proposal: Establish the journey corpus and baseline study + +## Outcome + +A checked-in, privacy-minimizing create/revise/review/share corpus and executable study +protocol produce an honest first-use and comprehension baseline from consented representative +participants, including failures and exclusions. + +## Context + +`OUT-02` requires a clean-machine install/create/reopen journey within ten minutes and +`OUT-03` requires at least 90% of at least ten representative primary users to identify four +page facts within one minute. No participant study exists. Existing examples and maintainer +browser QA are product fixtures, not representative-user evidence. Codex can prepare the +corpus, runner, consent/data-handling protocol, blank result records, and diagnostics, but it +cannot invent participants, consent, elapsed times, answers, or outcomes. + +## Scope + +- In scope: versioned create/revise/review/share tasks; neutral participant instructions; + primary-user inclusion criteria and conflict-of-interest disclosure; consent and withdrawal + procedure; clean-machine and one-minute protocols; machine-readable schemas; deterministic + validation and aggregation; pseudonymous raw records; complete pass/fail/excluded reporting; + retained dated summary; support for at least ten primary participants. +- Out of scope: recruiting or impersonating participants, collecting unnecessary names or + private artifact content, opt-out telemetry, claiming secondary-reviewer coverage before a + collaboration release, altering product behavior to improve the first baseline, or calling + maintainer self-testing representative-user evidence. + +## Risks and rollback + +- Risk: leading prompts, cherry-picked fixtures, non-representative participants, inconsistent + timing, or omitted failures could inflate comprehension; raw notes could retain personal or + proprietary information; the released package may differ from the tested bytes. +- Rollback: invalidate and retain the flawed run as failed evidence, revise the protocol in a + reapproved packet, and rerun with a new study identifier. Removing a summary never converts + missing evidence into a pass, and participation withdrawal follows the documented deletion + path for raw records. + +## Validation plan + +Before recruitment, a maintainer reviews task neutrality, consent language, representative +criteria, data minimization, timing, and scoring. The harness is verified with synthetic +records explicitly labeled as test fixtures. Validation requires real consented participants: +the OUT-02 report identifies tested package bytes/platform and all elapsed outcomes; the +OUT-03 report includes at least ten eligible primary users and reaches the specified threshold +for all four facts, with exclusions and non-responses visible. Until then, both baselines +remain missing and Phase 0 fails. diff --git a/specs/archive/2026-08-16-journey-corpus-and-baseline/tasks.md b/specs/archive/2026-08-16-journey-corpus-and-baseline/tasks.md new file mode 100644 index 0000000..89bc221 --- /dev/null +++ b/specs/archive/2026-08-16-journey-corpus-and-baseline/tasks.md @@ -0,0 +1,11 @@ +# Tasks: Establish the journey corpus and baseline study + +- [x] Confirm proposal validation and human approval. +- [x] Check in the corpus, consent/withdrawal protocol, inclusion rubric, and blank record schema. +- [x] Add deterministic schema, scoring, threshold, exclusion, and redaction tests. +- [x] Implement a dependency-free validator/aggregator that cannot turn missing runs into passes. +- [x] Record the product owner's decision not to run OUT-02/OUT-03 for public preview; retain + zero participants and incomplete certification status without substituting synthetic users. +- [x] Record validation and verification evidence. +- [x] Add `specs/current/journey-evidence.spec.md` and reconcile Phase 0 status. +- [x] Run repository validation and archive the packet. diff --git a/specs/archive/2026-08-16-platform-security-privacy-policy/change.json b/specs/archive/2026-08-16-platform-security-privacy-policy/change.json new file mode 100644 index 0000000..fca6391 --- /dev/null +++ b/specs/archive/2026-08-16-platform-security-privacy-policy/change.json @@ -0,0 +1,39 @@ +{ + "schemaVersion": 1, + "id": "platform-security-privacy-policy", + "title": "Publish platform, security, privacy, and release policy", + "lane": "high-risk", + "status": "archived", + "affectedRequirements": [ + "SEC-01", + "SEC-10", + "PRIV-01", + "PRIV-02", + "PRIV-03", + "PRIV-04", + "PRIV-05", + "PRIV-06", + "PRIV-07", + "COMPAT-01", + "DIST-03", + "DIST-04", + "DIST-05", + "DIST-06", + "DIST-07" + ], + "currentSpecs": [ + "specs/current/governance-policy.spec.md" + ], + "currentSpecsUpdated": true, + "approval": { + "by": "bitgorust", + "at": "2026-08-16T08:17:19Z" + }, + "withdrawal": { + "by": "", + "at": "", + "reason": "" + }, + "createdAt": "2026-08-16", + "archivedAt": "2026-08-16" +} diff --git a/specs/archive/2026-08-16-platform-security-privacy-policy/delta.md b/specs/archive/2026-08-16-platform-security-privacy-policy/delta.md new file mode 100644 index 0000000..eedef6e --- /dev/null +++ b/specs/archive/2026-08-16-platform-security-privacy-policy/delta.md @@ -0,0 +1,384 @@ +# Specification delta: Publish platform, security, privacy, and release policy + +## MODIFIED + +### Requirement: SEC-01 + +The repository MUST publish a dated, versioned threat model for portable pages, trusted HTML, +filesystem inputs, loopback services, deployment adapters, public-static targets, hosted +content/control planes, audience and identity changes, mutable collaboration state, and +connectors. Each boundary MUST name assets, authorities, untrusted inputs, threats, controls, +tests, residual risk, owner, and review trigger; planned boundaries MUST remain unshipped. + +#### Scenario: Normal behavior + +- **Given:** a release candidate uses only modeled boundaries with current mapped controls and tests +- **When:** the security maintainer reviews the threat model +- **Then:** the candidate records each applicable boundary as covered with dated evidence + +#### Scenario: Failure or refusal + +- **Given:** a new or changed trust boundary lacks threats, controls, owner, or adversarial tests +- **When:** release evidence is evaluated +- **Then:** the affected capability and release claim fail closed + +#### Scenario: Relevant boundary + +- **Given:** the model describes a future hosted or connector boundary not implemented now +- **When:** current capability status is generated +- **Then:** the boundary remains planned and cannot be cited as shipped protection + +### Requirement: SEC-10 + +The project MUST publish `SECURITY.md` with a verified private GitHub vulnerability-reporting +path, severity rubric, response targets, disclosure coordination, current supported-version +window, credential/key-rotation steps, and compromised-release containment and recovery. If +private reporting is disabled or unreachable, documentation MUST say reporting is unavailable +and production-readiness gates MUST fail rather than direct sensitive details to public issues. + +#### Scenario: Normal behavior + +- **Given:** private reporting is enabled and the current minor is supported +- **When:** a reporter follows `SECURITY.md` +- **Then:** the report reaches maintainers privately and the documented triage, disclosure, and recovery process applies + +#### Scenario: Failure or refusal + +- **Given:** private reporting is disabled, the version is unsupported, or response ownership is absent +- **When:** a security-readiness claim is checked +- **Then:** the failure is visible and production readiness is refused + +#### Scenario: Relevant boundary + +- **Given:** a release credential or published version is suspected compromised +- **When:** the playbook is invoked +- **Then:** publishing pauses, affected authority is revoked or rotated, users receive bounded guidance, and replacement provenance is verified before resumption + +### Requirement: PRIV-01 + +A versioned inventory MUST list every stored or transmitted field category for local, +public-static, authenticated, connector, journey-study, and release-evidence modes with its +purpose, controller/operator, processor/recipient, location, sensitivity, retention trigger, +and deletion path. Unavailable modes MUST describe planned fields separately from current flows. + +#### Scenario: Normal behavior + +- **Given:** a currently selectable capability stores or sends a field +- **When:** its inventory entry is reviewed +- **Then:** all required ownership, purpose, location, sensitivity, retention, and deletion fields are present and current + +#### Scenario: Failure or refusal + +- **Given:** a collected field lacks a capability-bound purpose or deletion path +- **When:** the capability or release is reviewed +- **Then:** collection and the associated claim are refused until the inventory and implementation agree + +#### Scenario: Relevant boundary + +- **Given:** an authenticated or connector field belongs only to an unshipped target +- **When:** the inventory is read +- **Then:** it is labeled planned and is not represented as current collection + +### Requirement: PRIV-02 + +Local creation MUST perform no default telemetry or analytics. User-selected public, hosted, +or connector actions MAY transmit only fields required for that action after disclosure; +optional analytics or studies require informed opt-in, and refusal MUST leave product +capability unchanged. + +#### Scenario: Normal behavior + +- **Given:** a user renders or opens a local artifact without selecting a network capability +- **When:** the workflow completes +- **Then:** no usage or artifact telemetry leaves the machine + +#### Scenario: Failure or refusal + +- **Given:** optional measurement has no affirmative informed consent +- **When:** collection would begin +- **Then:** no measurement record is sent or retained and functionality remains available + +#### Scenario: Relevant boundary + +- **Given:** the user explicitly deploys to a named provider +- **When:** required deployment fields cross the provider boundary +- **Then:** they are disclosed and classified as capability data, not hidden analytics consent + +### Requirement: PRIV-03 + +Credentials, tokens, grants, identity headers, security reports, and administrative secrets +MUST be excluded from portable pages, exports, browser configuration, public deployment trees, +evidence, and user-facing diagnostics. Final bytes and staged metadata MUST be scanned before +audience expansion; overrides MUST be targeted, explicit, and auditable. + +#### Scenario: Normal behavior + +- **Given:** a page and its staged metadata contain no detected secret material +- **When:** the final audience-bound bytes are scanned +- **Then:** publication may continue under the selected authority and the scan result is recorded + +#### Scenario: Failure or refusal + +- **Given:** a credential or private security detail is detected in any audience-bound field +- **When:** publish, export, evidence retention, or deployment is attempted +- **Then:** the operation fails without writing or exposing the secret + +#### Scenario: Relevant boundary + +- **Given:** an authorized maintainer approves a narrowly identified false-positive override +- **When:** the exact bytes are rescanned +- **Then:** only that target proceeds and the override does not become permission for another artifact, field, or audience + +### Requirement: PRIV-04 + +Every deployment surface MUST disclose the selected target's operator/controller boundary, +known storage location, third-party recipients, residency controls, and unsupported compliance +claims before data moves. User-operated GitHub and Cloudflare targets MUST NOT imply a project- +operated service, fixed region, backup promise, or SLA. + +#### Scenario: Normal behavior + +- **Given:** a user selects a public-static or future hosted target +- **When:** preflight presents the data boundary +- **Then:** operator, provider, known location/recipients, and residency limitations are visible before confirmation + +#### Scenario: Failure or refusal + +- **Given:** operator, region, or recipient information is unknown or contradicted by provider evidence +- **When:** documentation or setup output is produced +- **Then:** it states unknown or unsupported and does not assert compliance + +#### Scenario: Relevant boundary + +- **Given:** the software is deployed by a user into their own provider account +- **When:** project support language is evaluated +- **Then:** the user remains the deployment operator and provider terms are not converted into project guarantees + +### Requirement: PRIV-05 + +The policy MUST name mode-specific list, export, correction where applicable, and deletion +paths for artifact-related personal data, including derived state and provider caches/backups. +Unavailable operations MUST be labeled unavailable; deletion MUST identify exact scope, +irreversible consequences, provider/history limitations, backup expiry where known, and a +bounded completion result before it can be claimed complete. + +#### Scenario: Normal behavior + +- **Given:** a user controls local artifacts or a supported provider target +- **When:** they invoke a documented data-rights operation +- **Then:** the exact covered data and completion result are reported with remaining copies and expiry disclosed + +#### Scenario: Failure or refusal + +- **Given:** derived data, git history, provider backup, or cache cannot be deleted by the product +- **When:** deletion is requested +- **Then:** the operation reports that limitation and cannot claim complete erasure + +#### Scenario: Relevant boundary + +- **Given:** correction would rewrite an immutable revision +- **When:** a user requests correction +- **Then:** the policy preserves history integrity, offers a new revision or scoped removal where supported, and explains the tradeoff + +### Requirement: PRIV-06 + +Logs, metrics, traces, support bundles, fixtures, study records, screenshots, benchmarks, and +release evidence MUST minimize content and use pseudonymous identifiers. Each evidence class +MUST have purpose, access boundary, retention or review trigger, and withdrawal/deletion rule; +private artifacts MUST NOT leave their deployment boundary without explicit authorization. + +#### Scenario: Normal behavior + +- **Given:** diagnostic or acceptance evidence is retained +- **When:** it crosses into the repository or a review system +- **Then:** only purpose-required fields remain, identity/content is minimized, and retention/access are recorded + +#### Scenario: Failure or refusal + +- **Given:** a screenshot, trace, or fixture contains private content without explicit authority +- **When:** retention is attempted +- **Then:** it is rejected or redacted before leaving the deployment boundary + +#### Scenario: Relevant boundary + +- **Given:** a consented study participant withdraws within the protocol's covered period +- **When:** withdrawal is processed +- **Then:** the pseudonymous raw record is deleted while aggregate history is handled exactly as disclosed + +### Requirement: PRIV-07 + +Public-sharing policy MUST provide abuse, takedown, and intellectual-property reporting and +handling, identify the operator responsible for a user-owned target, preserve required asset +licenses/attribution, avoid publishing ambiguous/private references, and state that immediate +global removal cannot be guaranteed across git history, forks, caches, or third-party copies. + +#### Scenario: Normal behavior + +- **Given:** a public artifact contains redistributable attributed material +- **When:** it is published through a supported target +- **Then:** attribution remains visible and the target-specific reporting path is documented + +#### Scenario: Failure or refusal + +- **Given:** material is private, unlicensed, ambiguously licensed, or subject to a valid takedown +- **When:** publication or continued availability is reviewed +- **Then:** publication is refused or the operator follows the scoped takedown process and records remaining-copy limits + +#### Scenario: Relevant boundary + +- **Given:** the target is a repository or provider account operated by the user +- **When:** an abuse report is made to the project +- **Then:** the project identifies the responsible operator and available escalation without falsely claiming unilateral deletion authority + +### Requirement: COMPAT-01 + +The support matrix MUST distinguish target, tested, supported, unsupported, and unverified +combinations for Node 24, current and oldest-supported stable OpenCode, current Ubuntu LTS, +current and previous macOS, Windows 11/WSL, and the latest two stable Chromium, Firefox, +Safari, Android Chrome, and iOS Safari generations where available. A cell is supported only +with exact version, date, environment, test scope, and retained result; initially one tested +stable OpenCode version MAY be both current and oldest-supported. + +#### Scenario: Normal behavior + +- **Given:** every claimed matrix cell has current dated host/browser evidence +- **When:** support documentation is generated +- **Then:** the exact combination and tested scope are labeled supported with an evidence link + +#### Scenario: Failure or refusal + +- **Given:** an OS, browser, Node, or OpenCode combination has no run or a failing run +- **When:** a release claim is checked +- **Then:** it remains unverified or unsupported and cannot inherit support from another platform + +#### Scenario: Relevant boundary + +- **Given:** only the current stable OpenCode release has been tested +- **When:** oldest-supported is reported +- **Then:** current and oldest-supported may be the same exact version, and no broader range is implied + +### Requirement: DIST-03 + +Release policy MUST require SemVer, Conventional Commits, reviewed release notes, an explicit +release-level label, migrations, known limits/failures, and links to evidence for parity or +production-readiness claims. A release MUST be blocked or its claim narrowed when the evidence +does not cover the selected level. + +#### Scenario: Normal behavior + +- **Given:** a candidate has a SemVer tag, conventional changes, and complete evidence for one release level +- **When:** release notes are reviewed +- **Then:** they name the level, migrations, known limits, and exact evidence without broader language + +#### Scenario: Failure or refusal + +- **Given:** notes claim parity, support, or readiness without required evidence +- **When:** the release gate runs +- **Then:** publication is refused until the claim is removed or the evidence passes + +#### Scenario: Relevant boundary + +- **Given:** a security fix requires immediate removal without a normal notice period +- **When:** SemVer/deprecation impact is assessed +- **Then:** the exception, migration guidance, and security rationale are recorded without disclosing active exploit details prematurely + +### Requirement: DIST-04 + +Release CI MUST build the exact packed bytes, emit CycloneDX JSON, record dependency +vulnerability and license disposition, publish through npm trusted publishing with provenance, +and verify registry signatures/attestation after publication. Generated metadata is evidence +of composition and origin, not proof of safety; missing or invalid output blocks the readiness claim. + +#### Scenario: Normal behavior + +- **Given:** an authorized public GitHub Actions release uses the exact tested package bytes +- **When:** CI publishes and performs post-publish verification +- **Then:** SBOM, audit/license results, registry integrity, and npm provenance link to the tag, commit, workflow, and package digest + +#### Scenario: Failure or refusal + +- **Given:** SBOM generation, audit, license policy, trusted publishing, signature, provenance, or digest verification fails +- **When:** the release gate evaluates the candidate +- **Then:** publication stops when possible and no production-readiness claim is made + +#### Scenario: Relevant boundary + +- **Given:** workflow configuration requests provenance but registry-side evidence has not been retrieved +- **When:** release evidence is reconciled +- **Then:** provenance remains unverified rather than passing from configuration alone + +### Requirement: DIST-05 + +Dependencies and vendored runtimes MUST be lockfile-pinned and reviewed for SPDX license, +known vulnerabilities, page-view network behavior, CSP effect, browser weight, update owner, +and removal path. Runtime dependencies default to permissive licenses; reciprocal, source- +available, unknown, or conflicting terms require explicit legal/maintainer review before use. +Already-created portable pages MUST remain usable after package dependency removal. + +#### Scenario: Normal behavior + +- **Given:** the exact lockfile and packed page runtimes have complete acceptable dispositions +- **When:** dependency policy runs +- **Then:** versions, licenses, vulnerabilities, network/CSP impact, owner, and removal path are retained as pass evidence + +#### Scenario: Failure or refusal + +- **Given:** a dependency is unpinned, unknown-license, policy-incompatible, critically vulnerable, or adds undeclared view-time network access +- **When:** build or release review occurs +- **Then:** the candidate is blocked until removed, fixed, or explicitly reapproved under a documented policy change + +#### Scenario: Relevant boundary + +- **Given:** the package or a heavy runtime is removed after an artifact was created +- **When:** the existing self-contained HTML is reopened +- **Then:** the page remains viewable without the removed package or network + +### Requirement: DIST-06 + +The supported-version policy MUST support security fixes for the current package minor only +until broader staffing and tests are approved. It MUST define deprecation notice of at least +one supported release except active exploits, end-of-life status, migration guidance, +vulnerability reporting, and the point at which unsupported versions stop receiving fixes. + +#### Scenario: Normal behavior + +- **Given:** a vulnerability affects the current supported minor +- **When:** maintainers triage it +- **Then:** it receives the documented response and fix/release process with coordinated guidance + +#### Scenario: Failure or refusal + +- **Given:** a version is outside the current supported minor +- **When:** support is requested or a claim is generated +- **Then:** it is labeled unsupported with upgrade guidance and no unstaffed fix promise + +#### Scenario: Relevant boundary + +- **Given:** removal closes an actively exploitable vulnerability +- **When:** normal deprecation notice would extend exposure +- **Then:** maintainers may remove immediately, document the security exception, and provide the safest feasible migration + +### Requirement: DIST-07 + +Release evidence MUST inventory licenses and attribution for source, dependencies, +documentation, generated examples, embedded assets, fonts, and benchmark references. Only +content with documented redistribution authority MAY ship or enter a public evidence corpus; +private or ambiguous reference artifacts remain linked or privately reviewed without copying. + +#### Scenario: Normal behavior + +- **Given:** every shipped file and retained public reference has a compatible license or documented authority +- **When:** package and evidence contents are inspected +- **Then:** required notices/attribution ship and the inventory records the disposition + +#### Scenario: Failure or refusal + +- **Given:** an asset, font, example, or reference has missing, ambiguous, or incompatible redistribution terms +- **When:** packaging or evidence retention is attempted +- **Then:** it is excluded until authority is resolved and the omission remains visible + +#### Scenario: Relevant boundary + +- **Given:** a Claude artifact may be viewed under account permission but not redistributed +- **When:** comparative evidence is retained +- **Then:** permitted prompts/scores or private review metadata are used and the artifact itself is not committed diff --git a/specs/archive/2026-08-16-platform-security-privacy-policy/design.md b/specs/archive/2026-08-16-platform-security-privacy-policy/design.md new file mode 100644 index 0000000..66568ae --- /dev/null +++ b/specs/archive/2026-08-16-platform-security-privacy-policy/design.md @@ -0,0 +1,79 @@ +# Design: Publish platform, security, privacy, and release policy + +Required for high-risk changes. + +## Context and constraints + +The policy must describe the repository and currently selectable modes without granting new +authority. Portable local files, local service state, GitHub Pages, and user-deployed +Cloudflare targets have different operators, storage, deletion behavior, and audience risk. +Future authenticated and connector modes remain target behavior, not shipped data flows. +The matrix must obey the target floor in `COMPAT-01` while refusing support claims without +same-change or current dated platform evidence. Supply-chain controls must bind to exact +packed bytes and preserve the existing no-new-dependency default. + +## Chosen design + +Create canonical `docs/support-policy.md`, `docs/security.md`, and `docs/data-governance.md` +records plus public `SECURITY.md`. The support document owns D-01 and D-06: Node 24; stable +OpenCode 1.x with the oldest-supported version equal to the current tested stable until two +versions are tested; current Ubuntu LTS, current and previous macOS, Windows 11/WSL; and the +latest two stable desktop/mobile browser generations where available. Each cell carries an +exact dated evidence link and defaults to unverified, never supported by family resemblance. + +Use npm trusted publishing from the public GitHub Actions repository for registry provenance, +CycloneDX JSON from the exact lockfile/install for the SBOM, `npm audit` plus registry +signature verification, and a recorded SPDX license disposition. The current minor alone is +supported until broader staffed tests exist. Deprecations receive at least one supported +release of notice except an active exploit. Release evidence retains failures and verifies +the registry attestation after publish. + +The data inventory is organized by mode and field category with purpose, controller/operator, +location/recipient, sensitivity, retention, and deletion. Local creation sends no telemetry. +User-operated public targets state provider implications and do not inherit a project SLA or +compliance claim. Evidence and study records are pseudonymous, purpose-bound, and deleted on +withdrawal according to their protocol. + +## Alternatives + +Rejected: declaring the product cross-platform from Node/library portability, because that is +not executed evidence. Rejected: supporting every historical minor, because no staffed host +matrix exists. Rejected: long-lived npm automation tokens when trusted publishing is +available. Rejected: a proprietary SBOM tool or new dependency, because npm emits standard +CycloneDX. Rejected: one undifferentiated privacy policy, because local, public-static, hosted, +and connector modes have different operators and data flows. Rejected: a public issue as the +only vulnerability channel, because it can disclose exploit details. + +## Trust, privacy, and failure boundaries + +Authored content, metadata, paths, URLs, provider output, identity headers, and connector +results remain untrusted. The threat model enumerates portable file, trusted HTML, filesystem, +loopback, deployment, public-static, hosted content/control, audience, mutable state, and +connector boundaries even when a future boundary is unavailable. Unshipped boundaries are +marked planned and cannot support a release claim. + +Private vulnerability reporting must be enabled before the public policy points to it; its +current disabled state is retained as a failed prerequisite. Reports stay in GitHub Security +Advisories, not public issues. Local artifacts and state remain user-controlled; GitHub +history and provider caches/backups make public deletion non-instant and must be disclosed. +No credential, private report, raw participant identity, or private artifact enters retained +public evidence. + +## Migration, rollout, and rollback + +Land documents and non-mutating checks first after approval. Enable provider-side private +reporting and trusted publishing only with explicit external-state authority, verify them, +then allow their status to pass. Matrix support is promoted one cell at a time only from +dated evidence. Rollback removes a new claim but preserves the evidence and known failure; +compromise response pauses publishing, revokes affected credentials/trusted configuration, +deprecates affected package versions with safe guidance, rebuilds from a reviewed commit, +and verifies new provenance before resumption. + +## Formal-method decision + +- Decision: property model for release-policy status transitions; no formal proof for prose quality. +- Property and rationale: a matrix/policy claim may move from unverified or failed to supported + only when exact dated evidence exists and all mandatory fields pass; missing, expired, + excluded, or provider-configuration evidence can never aggregate to pass. +- Model/evidence path: dependency-free table-driven policy validator and tests to be added + after approval; human review remains required for whether the selected scope and policy are appropriate. diff --git a/specs/archive/2026-08-16-platform-security-privacy-policy/evidence.md b/specs/archive/2026-08-16-platform-security-privacy-policy/evidence.md new file mode 100644 index 0000000..9302f7b --- /dev/null +++ b/specs/archive/2026-08-16-platform-security-privacy-policy/evidence.md @@ -0,0 +1,158 @@ +# Evidence: Publish platform, security, privacy, and release policy + +This record separates policy publication from current operational evidence. Failed and +unverified results remain visible and block the affected support, certification, or hard-gate +claim. Under the later approved public-preview contract, certification-only participant and +target-matrix gaps do not block unsupported preview distribution. + +## Requirement: SEC-01 +- Validation: `bitgorust` approved the boundary set, fail-closed semantics, owner/review + triggers, and planned-versus-current distinction on 2026-08-16. +- Verification: the versioned threat model covers all ten required current, partial, and + planned boundaries; deterministic tests reject a missing boundary. +- Result: pass for Phase 0 policy; residual implementation risks remain explicit. +- Evidence: [@test](test/governance-policy.test.ts), [@manual](docs/threat-model.md) + +## Requirement: SEC-10 +- Validation: the severity, response, disclosure, rotation and compromised-release policy was + approved on 2026-08-16, including refusal while private intake is absent. +- Verification: after explicit authorization on 2026-08-16, authenticated repository + administrator `bitgorust` enabled GitHub private vulnerability reporting; a separate + provider read returned `enabled: true`, and the public policy links the private advisory + form while continuing to prohibit sensitive public issues. +- Result: pass for the Phase 0 private-intake policy and provider prerequisite; release-level + response ownership and the remaining production gates are evaluated separately. +- Evidence: [@test](test/governance-policy.test.ts), + [@manual](docs/evidence/governance/provider-status-2026-08-16.md) + +## Requirement: PRIV-01 +- Validation: the capability/mode inventory and operator/controller wording were approved on + 2026-08-16. +- Verification: machine validation requires all eight current/planned modes and every purpose, + operator/recipient, location, sensitivity, retention and deletion field. +- Result: pass for Phase 0 inventory publication. +- Evidence: [@test](test/governance-policy.test.ts), [@manual](docs/data-governance.md) + +## Requirement: PRIV-02 +- Validation: no-default-telemetry and affirmative opt-in wording was approved on 2026-08-16. +- Verification: code inspection found no project telemetry/analytics path; portable files use + strict `connect-src 'none'`, while loopback bridge requests exist only when served. +- Result: pass for current local creation; explicit provider deploy remains capability data. +- Evidence: [@test](test/render.test.ts), [@manual](docs/data-governance.md) + +## Requirement: PRIV-03 +- Validation: secret/output exclusions and narrow override wording were approved. +- Verification: deterministic tests cover authored content, title overrides, frontmatter + metadata before manifest/gallery writes, all current top-level public files including JSON, + provider target configuration, and stale files in reused GitHub/Cloudflare staging trees. + The exact-invocation force override is never persisted. +- Result: pass for current portable and public-deployment surfaces; unshipped authenticated, + connector, export, and support-bundle boundaries remain planned rather than inferred. +- Evidence: [@test](test/guard.test.ts), [@test](test/plugin.test.ts), + [@test](test/cli.test.ts), [@test](test/github-pages.test.ts), + [@test](test/cloudflare.test.ts) + +## Requirement: PRIV-04 +- Validation: the user-operator and no-residency/SLA/compliance wording was approved. +- Verification: canonical policies and Cloudflare guide disclose current boundaries, but CLI + deployment preflight does not yet present the complete inventory before data moves. +- Result: partial. +- Evidence: [@test](test/governance-policy.test.ts), [@manual](docs/hosted-cloudflare.md) + +## Requirement: PRIV-05 +- Validation: scoped deletion, immutable-history and provider-copy limitations were approved. +- Verification: the policy names current manual paths and explicitly marks provider-wide + receipts, backup expiry, authenticated and connector operations unavailable. +- Result: partial; policy is published, but bounded end-to-end deletion operations are absent. +- Evidence: [@manual](docs/data-governance.md), [@test](test/governance-policy.test.ts) + +## Requirement: PRIV-06 +- Validation: minimization, access, retention and withdrawal rules were approved. +- Verification: governance validation covers release/study inventory; journey tests reject + identity fields and redact participant codes/answers from aggregates. +- Result: partial; broad log/screenshot/support-bundle enforcement is not implemented. +- Evidence: [@test](test/journey-study.test.ts), [@manual](docs/data-governance.md) + +## Requirement: PRIV-07 +- Validation: user-operator responsibility, attribution and remaining-copy limits were + approved. +- Verification: a public abuse/IP process is published; no project-operated target or global + deletion authority is claimed. The machine-checked redistribution inventory binds all 21 + retained project-generated assets to exact hashes and MIT provenance, verifies zero embedded + fonts, keeps external benchmark material link-only, and removed the unlicensed local copy. +- Result: pass for Phase 0 public-sharing policy and current repository disposition; real + operator handling remains future operational evidence. +- Evidence: [@manual](docs/data-governance.md), + [@manual](docs/evidence/governance/redistribution-2026-08-16.md), + [@test](test/governance-policy.test.ts) + +## Requirement: COMPAT-01 +- Validation: Node 24, the target OS/browser envelope and exact-evidence promotion rule were + approved. +- Verification: an exact Ubuntu 24.04.4/Node 24.19.0/OpenCode 1.18.18/Chromium technical run + covers the retained CI tarball, tool discovery, CLI render, offline interaction, and package + removal. It lacks Firefox, latest-two-browser, and consented first-use coverage. Every target + cell remains unverified and there are zero supported complete cells. +- Result: fail for the target matrix. +- Evidence: [@test](test/governance-policy.test.ts), + [@manual](docs/evidence/governance/ubuntu-packed-observation-2026-08-16.md) + +## Requirement: DIST-03 +- Validation: capability-level, SemVer, notes/migration/limits and claim-narrowing rules were + approved. +- Verification: the canonical support policy and release template contain the required fields; + this packet is not itself a release decision. +- Result: pass for Phase 0 release policy. +- Evidence: [@manual](docs/support-policy.md), [@manual](docs/release-evidence-template.md) + +## Requirement: DIST-04 +- Validation: npm trusted publishing, CycloneDX, audit/signature/license and exact-byte binding + were approved. +- Verification: the baseline found 8 vulnerabilities. The approved remediation candidate now + reports zero audit findings, exact hash-bound license dispositions, 212 verified signatures, + 22 attestations, a CycloneDX SBOM, and candidate provenance bound to packed bytes. Run + 31956792983 retained the exact merge-candidate tarball and evidence. After explicit + authorization and npm package-owner authentication, provider readback verified the exact + `bitgorust/opencode-artifacts` / `publish.yml` trusted publisher with `createPackage` + permission. Published 0.14.3 provenance remains verified only for its own tag/commit; exact + Published 0.14.4 registry bytes are independently identical to the retained tag tarball; + its package signature and SLSA provenance bind `v0.14.4`, commit `a5ee65a`, `publish.yml`, + and run `31961711046`. The run retained its final singleton-array parser failure, and the + corrected regression-tested verifier passes that exact response. +- Result: pass for public-preview supply-chain gates; production readiness remains unclaimed. +- Evidence: [@manual](docs/evidence/governance/supply-chain-2026-08-16.md), + [@manual](docs/evidence/governance/renderer-remediation-2026-08-16.md), + [@manual](docs/evidence/governance/provider-status-2026-08-16.md), + [@test](test/release-integrity.test.ts) + +## Requirement: DIST-05 +- Validation: license, vulnerability, network/CSP, weight, owner and removal rules were + approved. +- Verification: the remediated runtime lockfile reports zero audit findings; exact path and + content-hash dispositions select the three previously unresolved licenses; compatibility, + size, and package-tree-removal checks pass. +- Result: pass for runtime dependency governance; production readiness remains blocked by + other open release gates. +- Evidence: [@manual](docs/evidence/governance/supply-chain-2026-08-16.md), + [@manual](docs/evidence/governance/renderer-remediation-2026-08-16.md), + [@test](test/release-integrity.test.ts), [@test](test/render.test.ts) + +## Requirement: DIST-06 +- Validation: the current-minor window and one-supported-release notice/active-exploit + exception were approved. +- Verification: the canonical support and security policies define version support, + deprecation, end-of-life, migration, reporting and fix boundaries. +- Result: pass for Phase 0 policy. +- Evidence: [@manual](docs/support-policy.md), [@manual](docs/security.md) + +## Requirement: DIST-07 +- Validation: redistribution authority, attribution and private-reference handling were + approved. +- Verification: repository MIT and runtime dependency dispositions pass. The complete current + documentation/example/asset/font/reference inventory is machine-checked; every retained + binary is hash-bound, there are no embedded fonts, external benchmarks are link-only, and + the ambiguously licensed local official screenshot was removed. +- Result: pass for the current repository distribution inventory. +- Evidence: [@manual](LICENSE), + [@manual](docs/evidence/governance/redistribution-2026-08-16.md), + [@test](test/governance-policy.test.ts) diff --git a/specs/archive/2026-08-16-platform-security-privacy-policy/proposal.md b/specs/archive/2026-08-16-platform-security-privacy-policy/proposal.md new file mode 100644 index 0000000..fe260b2 --- /dev/null +++ b/specs/archive/2026-08-16-platform-security-privacy-policy/proposal.md @@ -0,0 +1,55 @@ +# Proposal: Publish platform, security, privacy, and release policy + +## Outcome + +Users and release maintainers can determine exactly which platforms and capabilities are +supported, what data each mode handles, how security/privacy reports and deletion work, and +which supply-chain evidence is required; unverified combinations and missing external setup +remain visibly unsupported. + +## Context + +Phase 0 has no support matrix, threat model, data inventory, telemetry/retention/deletion or +abuse policy, private vulnerability path, supported-version policy, or accepted supply-chain +decision. That makes `D-01` and `D-06` open and prevents honest release claims. Current dated +facts are narrower than the target: OpenCode 1.18.18 is both the locally verified and registry +stable version as of 2026-08-16; CI targets Node 24 on Ubuntu; the current shell is Node 18 and +is not platform evidence; macOS, Windows, Safari, mobile, and a second stable host have not +been run; GitHub private vulnerability reporting is disabled; and the publish workflow asks +npm for provenance but its registry-side trusted-publisher result has not been verified. + +## Scope + +- In scope: one dated support matrix separating target, tested, supported, unsupported, and + unverified states; D-01 policy and update trigger; a versioned threat model; a capability- + and mode-specific data inventory; no-default-telemetry statement; retention, deletion, + disclosure, public abuse/takedown, and evidence-data rules; `SECURITY.md`; severity, + response, rotation, and compromised-release playbooks; current-minor support/deprecation/ + end-of-life policy; D-06 choice of npm trusted publishing, CycloneDX JSON SBOM, vulnerability + and license disposition; exact status reconciliation across README and contract docs. +- Out of scope: claiming tests on unavailable OS/browser combinations; enabling hosted or + connector capabilities; promising legal compliance or residency; inventing an email or + response team; changing the CSP, Publisher interface, plugin tool arguments, package version, + or npm/GitHub settings without explicit authority; releasing or deleting packages. + +## Risks and rollback + +- Risk: documentation could overstate unrun platforms, imply service-operator guarantees for + user-operated targets, expose report contents, promise unstaffed response times, or treat + generated SBOM/provenance as proof that dependencies are safe. Narrow support may reveal + that the current published package lacks a production-readiness claim. +- Rollback: revert the policy documents and release automation before any new claim, keeping + prior evidence and failures. External enablement (private vulnerability reporting or npm + trusted publishing) requires a separate authorized checkpoint and is reversed in provider + settings only by an authorized maintainer. No user data is migrated by this packet. + +## Validation plan + +Accountable maintainers review and approve the supported scope, controller/operator wording, +retention/deletion consequences, security response targets, license rules, and current-minor +support window. Verification includes deterministic claim/status checks, threat-model +coverage, repository secret scanning, license/vulnerability/SBOM dry runs, package inspection, +and dated real host/browser results for every supported matrix cell. Provider settings and +published provenance are recorded only after authorized real checks. Any missing platform, +private-reporting configuration, or provenance result remains failed/unverified and blocks +the Phase 0 gate rather than being inferred from workflow configuration. diff --git a/specs/archive/2026-08-16-platform-security-privacy-policy/tasks.md b/specs/archive/2026-08-16-platform-security-privacy-policy/tasks.md new file mode 100644 index 0000000..dfe254d --- /dev/null +++ b/specs/archive/2026-08-16-platform-security-privacy-policy/tasks.md @@ -0,0 +1,13 @@ +# Tasks: Publish platform, security, privacy, and release policy + +- [x] Confirm proposal validation and human approval. +- [x] Publish canonical support/D-01/D-06, security/threat, data-governance, and public vulnerability policies. +- [x] Add deterministic policy completeness, matrix-evidence, inventory, and claim-consistency tests. +- [x] Generate and inspect CycloneDX, vulnerability, signature, license, and packed-byte outputs. +- [x] Retain exact available host/browser observations and leave every unavailable target cell + unverified; defer support promotion to certification rather than blocking public preview. +- [x] Obtain explicit authority before enabling GitHub private vulnerability reporting or changing npm trusted publishing. +- [x] Verify external reporting/provenance state rather than inferring it from configuration; retain failed/unverified state. +- [x] Record validation and verification evidence. +- [x] Add `specs/current/governance-policy.spec.md` and reconcile README, roadmap, traceability, hosting, comparison, and release template. +- [x] Run repository validation and archive the packet. diff --git a/specs/archive/2026-08-16-public-preview-release-gate/change.json b/specs/archive/2026-08-16-public-preview-release-gate/change.json new file mode 100644 index 0000000..8567614 --- /dev/null +++ b/specs/archive/2026-08-16-public-preview-release-gate/change.json @@ -0,0 +1,31 @@ +{ + "schemaVersion": 1, + "id": "public-preview-release-gate", + "title": "Decouple public preview from certification evidence", + "lane": "high-risk", + "status": "archived", + "affectedRequirements": [ + "OUT-02", + "OUT-03", + "OUT-04", + "COMPAT-01", + "DIST-03", + "QUAL-08" + ], + "currentSpecs": [ + "specs/current/governance-policy.spec.md", + "specs/current/journey-evidence.spec.md" + ], + "currentSpecsUpdated": true, + "approval": { + "by": "bitgorust", + "at": "2026-08-16T16:50:30Z" + }, + "withdrawal": { + "by": "", + "at": "", + "reason": "" + }, + "createdAt": "2026-08-16", + "archivedAt": "2026-08-16" +} diff --git a/specs/archive/2026-08-16-public-preview-release-gate/delta.md b/specs/archive/2026-08-16-public-preview-release-gate/delta.md new file mode 100644 index 0000000..47fe46a --- /dev/null +++ b/specs/archive/2026-08-16-public-preview-release-gate/delta.md @@ -0,0 +1,164 @@ +# Specification delta: Decouple public preview from certification evidence + +## MODIFIED + +### Requirement: OUT-02 + +A certified local-artifact-core or later release MUST retain a first-time-user run on every +claimed supported clean machine: install the exact released package, create an offline +artifact, and reopen it from only README instructions in at most ten minutes, without a +checkout or hosting account. A public-preview distribution MAY proceed without this baseline +only when release evidence records OUT-02 as incomplete, claims zero supported cells and no +first-use usability result, and labels every release surface uncertified. Synthetic agent +probes MAY diagnose the instructions but MUST remain test-only. + +#### Scenario: Normal behavior + +- **Given:** a candidate seeking certified local-artifact-core status +- **When:** its first-use evidence is evaluated +- **Then:** every claimed supported platform has an eligible passing human run against the exact package and README + +#### Scenario: Failure or refusal + +- **Given:** no eligible first-time-user baseline exists and the owner refuses to conduct one +- **When:** an otherwise hard-gate-clean candidate is evaluated for public preview +- **Then:** preview may proceed with OUT-02 visibly incomplete, zero support/usability claim, and no certification label + +#### Scenario: Relevant boundary + +- **Given:** Kimi or another automated agent completes the first-use steps +- **When:** evidence is aggregated +- **Then:** the run is labeled synthetic diagnostic evidence and contributes nothing to OUT-02 certification + +### Requirement: OUT-03 + +A certified local-artifact-core or later release MUST retain the checked-in comprehension +baseline from at least ten consented representative primary users, with at least 90% meeting +the existing four-fact/one-minute threshold. A public-preview distribution MAY proceed without +that baseline only when OUT-03 remains incomplete, no comprehension or usability claim is +made, and automated/model probes are labeled synthetic and excluded. + +#### Scenario: Normal behavior + +- **Given:** at least ten eligible representative-user records for a certification candidate +- **When:** the approved corpus is scored +- **Then:** certification passes only when at least 90% identify all four facts within one minute without assistance + +#### Scenario: Failure or refusal + +- **Given:** the owner will not recruit participants or fewer than ten eligible records exist +- **When:** an otherwise hard-gate-clean candidate is evaluated for public preview +- **Then:** preview may proceed while OUT-03 remains visibly incomplete and no human-comprehension claim is made + +#### Scenario: Relevant boundary + +- **Given:** one or many model sessions answer the comprehension prompts +- **When:** preview and certification evidence are produced +- **Then:** their results may inform QA but never enter the representative-user denominator + +### Requirement: OUT-04 + +Public preview is a non-certified distribution state that MAY precede the separately releasable +local artifact core, local collaboration, public snapshots, authenticated collaboration, and +connector-capable artifact levels. A public preview MUST NOT claim or accumulate a certified +level. Each certified level continues to require every requirement assigned to that level. + +#### Scenario: Normal behavior + +- **Given:** exact preview hard gates pass while certification evidence is incomplete +- **When:** source or an npm package is made public +- **Then:** it is labeled public preview, unsupported and uncertified, with missing evidence linked + +#### Scenario: Failure or refusal + +- **Given:** a preview candidate is described as local artifact core, supported, production-ready, or parity-certified +- **When:** release validation runs +- **Then:** publication is refused regardless of automated test success + +#### Scenario: Relevant boundary + +- **Given:** a later candidate seeks a certified release level +- **When:** accumulated requirements are evaluated +- **Then:** prior preview publication supplies no waiver, pass, or support evidence + +### Requirement: COMPAT-01 + +The support matrix MUST name exact tested, unverified, unsupported, and supported Node, +OpenCode, OS, desktop-browser, and mobile-browser ranges. Certified local-artifact-core and +later levels retain the target floor of Node 24+, current and oldest-supported stable OpenCode, +current Ubuntu LTS, current and previous macOS, Windows 11, and latest-two stable +Chromium/Firefox/Safari where available. Public preview MAY have zero supported cells only when +every surface says so and exact technical observations are not promoted into support. + +#### Scenario: Normal behavior + +- **Given:** a certification candidate with the full dated target matrix +- **When:** compatibility is evaluated +- **Then:** only exact combined passing cells become supported and the certified claim matches them + +#### Scenario: Failure or refusal + +- **Given:** a public-preview candidate has one narrow technical observation and no complete cells +- **When:** preview evidence is generated +- **Then:** it records zero supported cells and keeps every target cell unverified without blocking preview publication + +#### Scenario: Relevant boundary + +- **Given:** a browser generation, OS, Node, or OpenCode version moves after a preview +- **When:** a later certification is considered +- **Then:** the old observation stays historical and cannot satisfy the new supported cell + +### Requirement: DIST-03 + +Every distribution MUST use SemVer and Conventional Commits, include reviewed release notes, +state its distribution/certification status and capabilities, name migrations and known +limits, and link exact evidence. Public preview additionally MUST pass the closed hard-gate +set for tests/build/checks, final-byte secret and CSP controls, vulnerability/license/ +redistribution disposition, private vulnerability intake, exact package identity, trusted +publishing, registry integrity and provenance. It MUST visibly record missing OUT-02, +OUT-03, support, parity, and production-readiness evidence. Certified releases continue to +require all evidence for their claimed level. + +#### Scenario: Normal behavior + +- **Given:** an authorized SemVer preview candidate whose complete hard-gate set passes +- **When:** release notes and evidence are finalized +- **Then:** the exact candidate may publish as unsupported public preview and post-publish bytes/provenance are verified + +#### Scenario: Failure or refusal + +- **Given:** any hard gate fails or the preview label/missing-evidence disclosure is absent +- **When:** publication is attempted +- **Then:** the workflow fails before registry mutation + +#### Scenario: Relevant boundary + +- **Given:** a preview is later superseded by a certification candidate +- **When:** release level is selected +- **Then:** the certification candidate re-runs its full applicable evidence and does not inherit a waiver + +### Requirement: QUAL-08 + +Release evidence MUST state failures, exclusions, flaky-test disposition, unsupported +platforms, and uncollected human evidence as visibly as successes. Documentation and README +claims MUST be checked against the requirement set before every distribution. Missing +OUT-02/OUT-03 and support evidence is permitted only for an explicitly uncertified public +preview; it remains incomplete rather than passed or not applicable. + +#### Scenario: Normal behavior + +- **Given:** a public-preview evidence record with passing hard gates and missing research/matrix evidence +- **When:** claim consistency is checked +- **Then:** the record passes only if every missing result and the unsupported/uncertified status are prominent + +#### Scenario: Failure or refusal + +- **Given:** missing human or platform evidence is omitted, marked pass, or marked not applicable +- **When:** preview validation runs +- **Then:** the evidence and release fail + +#### Scenario: Relevant boundary + +- **Given:** synthetic Kimi results and exact Ubuntu technical observations are retained +- **When:** the public preview is summarized +- **Then:** they appear as diagnostics with their scope and cannot erase the missing human or target-matrix evidence diff --git a/specs/archive/2026-08-16-public-preview-release-gate/design.md b/specs/archive/2026-08-16-public-preview-release-gate/design.md new file mode 100644 index 0000000..89dc861 --- /dev/null +++ b/specs/archive/2026-08-16-public-preview-release-gate/design.md @@ -0,0 +1,74 @@ +# Design: Decouple public preview from certification evidence + +Required for high-risk changes. + +## Context and constraints + +Public npm bytes and source copies are irreversible once consumed, while a public repository +does not by itself imply support or production readiness. The current contract conflates +distribution with the first certified capability level, so unavailable human research and +target machines deadlock unrelated engineering and security remediation. The redesign must +not fabricate participants, convert missing evidence to not-applicable, weaken hard security +or supply-chain gates, or let preview history count toward later certification. + +## Chosen design + +Use an explicit monotonic release-state machine: + +1. `development` has no distribution claim. +2. `preview-candidate` requires the closed hard-gate set but may retain incomplete human and + support evidence. +3. `public-preview` is public, unsupported, uncertified, and bound to exact SemVer/tag/commit/ + bytes/workflow/provenance evidence. +4. `certified-local-core` and later accumulated levels retain the existing participant, + platform, behavior, and quality gates. + +Preview eligibility is represented and tested as data rather than inferred from prose. The +hard-gate set includes build/test/structural/package success, current vulnerability and license +disposition, redistribution inventory, final-byte security controls, verified private report +intake, exact candidate coordinate, trusted OIDC publication, registry byte equality, +signature, and provenance. Missing OUT-02, OUT-03, full support, and comparative evidence is +allowed only in the explicit preview state and must remain `incomplete`/`unverified`. + +## Alternatives + +- Count Kimi/model sessions as participants: rejected because it does not measure a + representative developer, violates consent/eligibility rules, and would fabricate the + denominator. +- Delete OUT-02/OUT-03 entirely: rejected because those remain valuable certification + outcomes even if the current owner declines to collect them. +- Keep every goal blocked until human/platform inputs appear: rejected by the product owner + because it prevents public inspection and unrelated engineering progress. +- Claim a narrow supported Ubuntu cell from the existing observation: rejected because the + run lacks the approved browser generations and human first-use evidence. + +## Trust, privacy, and failure boundaries + +Preview does not weaken content, credential, dependency, provenance, or reporting controls; +public distribution increases their importance. A release must fail before tagging/publishing +if a hard gate fails. User-facing surfaces must say unsupported and uncertified, name the +tested observation separately from support, and link missing evidence. npm publication is +irreversible for a name/version pair, so exact authorization, SemVer coordination, and +post-publish verification remain mandatory. No participant identity or model credential is +stored in public evidence. + +## Migration, rollout, and rollback + +Roll out first as contract, validator, and release-evidence changes on the goal branch. Generate +a complete preview candidate record and dry-run the transition before creating a tag. After +explicit release authority and npm trusted-publisher verification, publish one patch preview, +verify registry bytes/provenance, and retain the result. Abort before registry mutation on any +failed hard gate. Rollback disables future preview transitions and uses deprecation plus a +corrective version; published bytes cannot be recalled reliably. + +## Formal-method decision + +- Decision: bounded state machine plus property-model enumeration. +- Property and rationale: for every combination of preview inputs, publication is allowed if + and only if every hard gate passes, the preview/unsupported disclosures are present, and + missing research/platform evidence remains non-pass. Certification additionally requires + its full evidence and can never inherit a preview waiver. Exhaustive Boolean enumeration is + tractable and directly protects the dangerous registry transition. +- Model/evidence path: extend `scripts/release-integrity.ts` and + `test/release-integrity.test.ts`; retain the exact preview record under + `docs/evidence/releases/`. diff --git a/specs/archive/2026-08-16-public-preview-release-gate/evidence.md b/specs/archive/2026-08-16-public-preview-release-gate/evidence.md new file mode 100644 index 0000000..b67a1af --- /dev/null +++ b/specs/archive/2026-08-16-public-preview-release-gate/evidence.md @@ -0,0 +1,80 @@ +# Evidence: Decouple public preview from certification evidence + +The product owner approved the proposal on 2026-08-16 after explicitly declining the human +study for this preview. The implementation retains missing and excluded results rather than +treating public preview as certification. Authenticated provider readback verifies the exact +npm trusted publisher. Published registry bytes, package signature, and SLSA provenance bind +the exact tag/commit/workflow, so the transition is `public-preview`. + +## Requirement: OUT-02 + +- Validation: `bitgorust` approved keeping the representative-user first-use baseline as a + future certification gate rather than a public-preview gate. +- Verification: the transition property tests permit an `incomplete` OUT-02 result only with + the preview, unsupported, uncertified, and missing-evidence disclosures present; the release + record exposes zero eligible participants. +- Result: incomplete for certification and intentionally non-blocking for public preview. +- Evidence: [@test](test/release-integrity.test.ts), + [@manual](docs/evidence/releases/2026-08-16-v0.14.4-preview.md) + +## Requirement: OUT-03 + +- Validation: `bitgorust` approved retaining the representative-user comprehension study for + future certification without substituting local Kimi/model sessions. +- Verification: the transition property tests permit an `incomplete` OUT-03 result only with + missing evidence visible and reject certification until it passes; the release record keeps + the participant count at zero. +- Result: incomplete for certification and intentionally non-blocking for public preview. +- Evidence: [@test](test/release-integrity.test.ts), + [@manual](docs/evidence/journeys/phase-0-baseline-status.md) + +## Requirement: OUT-04 + +- Validation: the approved proposal makes public preview a distribution state outside the + accumulated certified capability levels. +- Verification: deterministic transition tests reject a preview certification claim and prove + that certified local core cannot inherit preview labels, unsupported status, or missing + OUT-02/OUT-03/support evidence. +- Result: pass for the implemented contract and exact public-preview transition. +- Evidence: [@test](test/release-integrity.test.ts), + [@manual](docs/requirements-traceability.md) + +## Requirement: COMPAT-01 + +- Validation: the approved scope permits zero supported platform/browser cells only for an + explicitly unsupported preview. +- Verification: governance claim checks require the README and support policy to expose + unsupported/uncertified preview status, while the candidate record lists the full matrix as + unverified. +- Result: pass for preview disclosure; incomplete for certification. +- Evidence: [@test](test/governance-policy.test.ts), + [@manual](docs/support-policy.md) + +## Requirement: DIST-03 + +- Validation: the approved design names a closed pre-publish and post-publish hard-gate set, + exact tag/version coordination, and forward-only rollback. +- Verification: property tests reject every failed gate at its applicable transition; the tag + workflow verifies tag/version agreement and retains exact release artifacts. Local tests, + build, structural checks, package review, audit, licenses, and redistribution pass for the + candidate. Authenticated npm readback verifies repository `bitgorust/opencode-artifacts`, + workflow `publish.yml`, and permission `createPackage`. Retained and independently + downloaded registry bytes are identical; the registry package signature and SLSA statement + bind `v0.14.4`, commit `a5ee65a`, `publish.yml`, and run `31961711046`. The run's visible + singleton-array parser failure is corrected and regression-tested in `b97a9b2`; the corrected + verifier passes the retained response. +- Result: pass for `public-preview`. +- Evidence: [@test](test/release-integrity.test.ts), + [@manual](docs/evidence/releases/2026-08-16-v0.14.4-preview.md) + +## Requirement: QUAL-08 + +- Validation: the approved proposal requires missing certification inputs to remain prominent + and never become pass or not applicable. +- Verification: claim-consistency tests require the public-preview disclosures; the candidate + record lists incomplete human, platform, accessibility, parity, performance, and operations + evidence alongside every passing technical result. +- Result: pass for the release documentation, including the visible post-publish parser failure + and incomplete certification evidence. +- Evidence: [@test](test/governance-policy.test.ts), + [@manual](docs/evidence/releases/2026-08-16-v0.14.4-preview.md) diff --git a/specs/archive/2026-08-16-public-preview-release-gate/proposal.md b/specs/archive/2026-08-16-public-preview-release-gate/proposal.md new file mode 100644 index 0000000..1717c6b --- /dev/null +++ b/specs/archive/2026-08-16-public-preview-release-gate/proposal.md @@ -0,0 +1,55 @@ +# Proposal: Decouple public preview from certification evidence + +## Outcome + +Permit an explicitly uncertified public-preview source and npm release after exact security, +privacy, redistribution, package-integrity, and supply-chain gates pass, without converting a +missing representative-user study or full supported-platform matrix into a pass. Public +preview is a distribution state, not a certified capability level. OUT-02/OUT-03 and complete +platform evidence remain prerequisites only for a future supported local-artifact-core (or +later) certification. + +## Context + +The repository and npm package are already public, but the current Goal 1/Phase 0 contract +blocks all subsequent delivery and any new release on at least ten consented representative +participants and the complete target platform/browser matrix. The product owner has stated +that they will not run OUT-02/OUT-03 and wants public delivery to continue. Counting Kimi or +another model as representative people would contradict the approved study protocol and +fabricate evidence. A separate, visibly unsupported preview state lets engineering and public +inspection continue without making that false claim. + +## Scope + +- In scope: distinguish public preview from certified release levels; change the Phase 0 and + Goal 1 stopping conditions; qualify OUT-02, OUT-03, COMPAT-01, DIST-03, and QUAL-08 release + applicability; add a machine-checked release-state/property model; update README, support, + traceability, roadmap, runbook, release evidence, and current specs; prepare an authorized + preview release only after the hard provider and exact-byte gates pass. +- Out of scope: calling a model a human participant; claiming support, production readiness, + parity, or a certified local artifact core; weakening CSP, secret scanning, vulnerability, + license, redistribution, provenance, or package-integrity gates; fabricating platform + results; changing runtime APIs; or treating a preview as an accumulated prerequisite for a + later certified level. + +## Risks and rollback + +- Risk: users may interpret a public package as supported; missing usability research may + persist indefinitely; an npm version cannot be reused or fully recalled after publication; + and a permissive preview transition could accidentally bypass a security or supply-chain + gate. Controls are an explicit preview label at every release surface, zero supported-cell + claims, a closed hard-gate allowlist, a property model, exact evidence, and refusal to + promote preview evidence into certification. +- Rollback: disable future preview tags, restore the certification-only gate through a new + approved packet, deprecate any affected npm version, and publish a corrective version and + notice. Already downloaded bytes and third-party copies cannot be revoked, so rollback is + forward-only and must remain visible in release evidence. + +## Validation plan + +The product owner's explicit refusal to conduct the human study and direction to permit public +delivery validates the product decision. Verification requires deterministic transition tests +showing that public preview accepts missing OUT-02/OUT-03/support evidence only when it remains +visibly incomplete, rejects every failed hard gate, and can never be labeled supported or +certified. A completed preview release record, exact CI/package outputs, provider readback, +registry digest, and provenance verify the first real transition. diff --git a/specs/archive/2026-08-16-public-preview-release-gate/tasks.md b/specs/archive/2026-08-16-public-preview-release-gate/tasks.md new file mode 100644 index 0000000..a82150a --- /dev/null +++ b/specs/archive/2026-08-16-public-preview-release-gate/tasks.md @@ -0,0 +1,10 @@ +# Tasks: Decouple public preview from certification evidence + +- [x] Confirm proposal validation and human approval. +- [x] Add the preview/certification transition property model and deterministic claim-consistency tests. +- [x] Update product spec, traceability, roadmap Phase 0, Goal 1 runbook, support/release policy, README, and release template. +- [x] Add a complete public-preview candidate evidence record with every hard gate and missing certification input visible. +- [x] Verify or configure the exact npm trusted publisher after package-owner authentication. +- [x] Publish only after the authorized preview candidate passes; retain registry-byte, signature, and provenance verification. +- [x] Record validation and verification evidence and update affected `specs/current/` files. +- [x] Run repository validation and archive the packet. diff --git a/specs/archive/2026-08-16-supply-chain-vulnerability-remediation/change.json b/specs/archive/2026-08-16-supply-chain-vulnerability-remediation/change.json new file mode 100644 index 0000000..7be55b0 --- /dev/null +++ b/specs/archive/2026-08-16-supply-chain-vulnerability-remediation/change.json @@ -0,0 +1,31 @@ +{ + "schemaVersion": 1, + "id": "supply-chain-vulnerability-remediation", + "title": "Remediate renderer dependency vulnerabilities and licenses", + "lane": "high-risk", + "status": "archived", + "affectedRequirements": [ + "RENDER-02", + "SEC-02", + "SEC-04", + "DIST-04", + "DIST-05", + "QUAL-06" + ], + "currentSpecs": [ + "specs/current/governance-policy.spec.md", + "specs/current/renderer-dependencies.spec.md" + ], + "currentSpecsUpdated": true, + "approval": { + "by": "bitgorust", + "at": "2026-08-16T14:57:27Z" + }, + "withdrawal": { + "by": "", + "at": "", + "reason": "" + }, + "createdAt": "2026-08-16", + "archivedAt": "2026-08-16" +} diff --git a/specs/archive/2026-08-16-supply-chain-vulnerability-remediation/delta.md b/specs/archive/2026-08-16-supply-chain-vulnerability-remediation/delta.md new file mode 100644 index 0000000..9b9c6ad --- /dev/null +++ b/specs/archive/2026-08-16-supply-chain-vulnerability-remediation/delta.md @@ -0,0 +1,143 @@ +# Specification delta: Remediate renderer dependency vulnerabilities and licenses + +## MODIFIED + +### Requirement: RENDER-02 + +The declarative format MUST render every documented Vega, Vega-Lite, and ECharts chart through +supported non-vulnerable runtime majors, with schema validation, inline actionable errors, and +compatibility evidence for canonical chart fixtures. + +#### Scenario: Supported chart + +- **Given:** A valid documented chart fixture. +- **When:** The upgraded renderer creates a portable page. +- **Then:** The chart renders with its documented data, labels, interactions, and accessibility metadata. + +#### Scenario: Invalid chart + +- **Given:** A malformed or unsupported chart specification. +- **When:** The renderer validates it. +- **Then:** Rendering fails with an inline actionable error and no partial executable payload. + +#### Scenario: Major-version boundary + +- **Given:** Every canonical chart fixture and documented chart kind. +- **When:** The runtime family crosses the approved major-version boundary. +- **Then:** Compatibility tests and representative browser smokes show no unexplained contract regression. + +### Requirement: SEC-02 + +Chart component payloads MUST remain untrusted across validation, transformation, tooltip, +expression, data-mutation, and browser-global boundaries and MUST fail closed when unsafe. + +#### Scenario: Ordinary untrusted content + +- **Given:** A chart contains user-controlled text and data. +- **When:** It is rendered and interacted with. +- **Then:** The content is treated as data and does not gain code or global-object authority. + +#### Scenario: Known exploit class + +- **Given:** A payload targets ECharts tooltip HTML or Vega expression/data mutation. +- **When:** The upgraded runtime processes it. +- **Then:** No script executes, no unauthorized mutation occurs, and the check records the refusal. + +#### Scenario: Encoded boundary payload + +- **Given:** The same payload uses nested, malformed, or encoded values at supported size limits. +- **When:** Validation and rendering run. +- **Then:** It remains inert or fails closed without bypassing size and encoding controls. + +### Requirement: SEC-04 + +Upgraded chart runtimes MUST operate under the existing strict CSP without `unsafe-eval`, raw +HTML passthrough, undeclared view-time network access, framing authority, or credential access. + +#### Scenario: Offline chart execution + +- **Given:** A valid chart in a self-contained page under the production CSP. +- **When:** The page is loaded without network access. +- **Then:** It renders through the interpreter path with no CSP violation or remote request. + +#### Scenario: Execution breakout + +- **Given:** A chart attempts expression, script, URL, or debug-global breakout. +- **When:** The page loads and the payload is exercised. +- **Then:** The attempt cannot execute or obtain control-plane credentials and the page exposes no Vega View global. + +#### Scenario: Runtime integration boundary + +- **Given:** The approved new runtime majors require integration changes. +- **When:** those changes are reviewed. +- **Then:** CSP is not weakened, `unsafe-eval` remains forbidden, and regression tests cover each changed boundary. + +### Requirement: DIST-04 + +Release CI MUST block the exact packed candidate unless its SBOM, provenance, dependency audit, +and explicit license dispositions are complete and mutually identify the same candidate bytes. + +#### Scenario: Clean candidate + +- **Given:** The approved dependency set and license branches pass every release scan. +- **When:** CI evaluates the packed tarball. +- **Then:** It emits the SBOM, audit, license, integrity, and provenance evidence before publication. + +#### Scenario: Unresolved finding + +- **Given:** Any known vulnerability, incompatible license, missing metadata, or hash mismatch is undisposed. +- **When:** CI evaluates the candidate. +- **Then:** Publication is blocked without a silent waiver. + +#### Scenario: Evidence identity boundary + +- **Given:** Scan outputs and a tarball were produced by the workflow. +- **When:** candidate integrity is checked. +- **Then:** Every record binds to the exact lockfile or tarball bytes and a mismatch fails closed. + +### Requirement: DIST-05 + +Renderer dependencies MUST be lockfile-pinned, upgraded together under compatibility and +security tests, and associated with reviewed license, network, CSP, and portability evidence. + +#### Scenario: Approved upgrade + +- **Given:** The proposal and exact license branches have human approval. +- **When:** The renderer family and lockfile are upgraded. +- **Then:** Tests and evidence cover license, network, CSP, compatibility, and runtime-size impact. + +#### Scenario: Unapproved license choice + +- **Given:** A package has missing or compound license metadata without a recorded exact choice. +- **When:** the license gate runs. +- **Then:** It fails and does not infer approval from a preferred permissive branch. + +#### Scenario: Package-removal boundary + +- **Given:** A page was generated before the installed package is removed. +- **When:** The page is opened from its self-contained bytes. +- **Then:** Its chart remains usable without the npm package or a view-time download. + +### Requirement: QUAL-06 + +Threat-model tests MUST include the chart-specific script-breakout, tooltip, expression, +data-mutation, global-gadget, network, and resource-boundary payload classes affected by the +renderer dependency upgrade. + +#### Scenario: Adversarial regression corpus + +- **Given:** Fixtures represent each cited chart exploit class. +- **When:** The threat-model suite runs on the candidate. +- **Then:** Every payload remains inert or is rejected with an asserted fail-closed result. + +#### Scenario: Security-test failure + +- **Given:** A fixture executes, fetches, mutates unauthorized state, or creates a dangerous global. +- **When:** The suite observes the effect. +- **Then:** The candidate fails before packing or publication. + +#### Scenario: Resource boundary + +- **Given:** Adversarial charts approach documented encoding and resource limits. +- **When:** Validation and browser execution run. +- **Then:** They complete within the bounded test budget or fail closed without weakening security controls. diff --git a/specs/archive/2026-08-16-supply-chain-vulnerability-remediation/design.md b/specs/archive/2026-08-16-supply-chain-vulnerability-remediation/design.md new file mode 100644 index 0000000..409bb15 --- /dev/null +++ b/specs/archive/2026-08-16-supply-chain-vulnerability-remediation/design.md @@ -0,0 +1,68 @@ +# Design: Remediate renderer dependency vulnerabilities and licenses + +Required for high-risk changes. + +## Context and constraints + +Chart specifications are user-controlled component payloads executed by inlined browser +runtimes. The current direct versions are ECharts 5.6.0, Vega 5.33.1, Vega-Lite 5.23.0, and +Vega-Embed 6.29.0. Their audit fix paths cross major versions. Portable pages must remain +offline, CSP must continue to forbid `unsafe-eval`, Markdown must not gain raw-HTML passthrough, +and old generated pages must not depend on the package remaining installed. + +The license scanner has separate uncertainty: `khroma` omits usable package metadata but its +installed license file is MIT text; `dompurify` offers Apache-2.0 or MPL-2.0; `json-schema` +offers BSD-3-Clause or AFL-2.1. The policy requires a recorded human choice rather than an +automatic preference for the least restrictive branch. + +## Chosen design + +Upgrade the four direct renderer packages as one compatibility set and keep Vega execution on +the interpreter path (`ast: true`). Add focused payload fixtures for ECharts tooltip names and +Vega expression/data mutation and debug-global gadget classes, then run the existing renderer, +CSP, offline, and canonical-example suite. Assert that the application does not export a Vega +View or debug object onto the global scope. + +Extend the license disposition table only after approval to bind each exceptional package to +the exact installed license-file hash and the approved branch: `khroma`/MIT, +`dompurify`/Apache-2.0, and `json-schema`/BSD-3-Clause. A hash or branch mismatch must fail +closed. Generate SBOM, audit, signature, license, and packed-artifact evidence from the same +candidate bytes before a release can pass. + +## Alternatives + +- Patch only the vulnerable leaf packages: rejected because the supported audit fixes require + major direct-package changes and npm cannot produce a clean supported tree on the old majors. +- Suppress or waive the advisories: rejected because user chart payloads exercise the affected + trust boundary and release policy requires explicit disposition. +- Replace the chart engines: rejected as a larger public-contract change with no Phase 0 need. +- Select compound license branches automatically: rejected because repository policy reserves + alternative and reciprocal-license choices for maintainer/legal review. + +## Trust, privacy, and failure boundaries + +All component payloads remain untrusted. Parsing, validation, and rendering must fail closed on +malformed or dangerous input, with no script execution, credential access, remote fetch, or +new global capability. License approval is limited to the named versions, exact license texts, +and recorded branches; it does not approve future hash or license changes. A failed audit, +license mismatch, regression test, CSP check, or browser smoke blocks the candidate before +publication. + +## Migration, rollout, and rollback + +Land only after proposal approval. Upgrade and test the renderer family together, then record +fresh audit and license evidence for the exact lockfile and packed tarball. Roll out through the +existing candidate workflow; there is no data migration. On compatibility or security failure, +revert the dependency change and keep release blocked. Existing self-contained HTML remains +readable because its runtime bytes were already inlined. + +## Formal-method decision + +- Decision: bounded property model. +- Property and rationale: no candidate can reach the releasable state unless vulnerability, + license, CSP, offline, adversarial-payload, compatibility, and packed-byte checks all pass; + every failure transitions to blocked without a waiver. A small property model is adequate + because the decision is a conjunction of deterministic gates rather than a concurrent state + system. +- Model/evidence path: extend `test/release-integrity.test.ts` with table-driven candidate-gate + cases and add renderer security fixtures/tests for the cited payload classes. diff --git a/specs/archive/2026-08-16-supply-chain-vulnerability-remediation/evidence.md b/specs/archive/2026-08-16-supply-chain-vulnerability-remediation/evidence.md new file mode 100644 index 0000000..5087011 --- /dev/null +++ b/specs/archive/2026-08-16-supply-chain-vulnerability-remediation/evidence.md @@ -0,0 +1,59 @@ +# Evidence: Remediate renderer dependency vulnerabilities and licenses + +The approved implementation passed locally and on the exact pull-request merge candidate. + +## Requirement: RENDER-02 + +- Validation: Current chart support is public behavior, so the major upgrade must preserve every documented chart kind. +- Verification: Canonical chart tests and representative Chromium smoke evidence cover both + renderer families, responsive layout, keyboard operation, and the intentional broken-spec + example. +- Result: pass locally. +- Evidence: [@test](test/renderer-security.test.ts), + [@manual](docs/evidence/governance/renderer-remediation-2026-08-16.md) + +## Requirement: SEC-02 + +- Validation: The cited exploit classes process user-controlled chart data at an existing untrusted boundary. +- Verification: Adversarial tooltip, expression, data-mutation, encoding, and global-authority + cases pass in deterministic tests and the browser smoke. +- Result: pass locally. +- Evidence: [@test](test/renderer-security.test.ts), + [@manual](docs/evidence/governance/renderer-remediation-2026-08-16.md) + +## Requirement: SEC-04 + +- Validation: Existing policy forbids script breakout, view-time network, and `unsafe-eval`; the upgrade cannot relax it. +- Verification: CSP, offline-network, interpreter-path, credential-isolation, and browser-global + checks pass without weakening the portable-file policy. +- Result: pass locally. +- Evidence: [@test](test/renderer-security.test.ts), + [@manual](docs/evidence/governance/renderer-remediation-2026-08-16.md) + +## Requirement: DIST-04 + +- Validation: Release CI already blocks on audit and license findings, exposing the exact remediation need. +- Verification: The clean local candidate and GitHub Actions merge candidate pass audit, + license, integrity, registry-signature, SBOM, and provenance-binding gates. Run 31956792983 + retained the exact tarball and every output for 14 days. +- Result: pass for candidate supply-chain evidence; no registry release was performed. +- Evidence: [@test](test/release-integrity.test.ts), + [@manual](docs/evidence/governance/renderer-remediation-2026-08-16.md) + +## Requirement: DIST-05 + +- Validation: Major renderer upgrades and alternative license branches require explicit compatibility and maintainer review. +- Verification: Approved hash-bound license dispositions, the lockfile update, compatibility + tests, the size delta, and a package-tree-removal browser smoke all pass. +- Result: pass locally. +- Evidence: [@test](test/release-integrity.test.ts), [@test](test/renderer-security.test.ts), + [@manual](docs/evidence/governance/renderer-remediation-2026-08-16.md) + +## Requirement: QUAL-06 + +- Validation: The advisory payload classes refine the existing script-breakout and resource-exhaustion threat requirements. +- Verification: Table-driven candidate-gate modeling and renderer adversarial regression tests + cover each required failure class. +- Result: pass locally. +- Evidence: [@model](test/release-integrity.test.ts), + [@test](test/renderer-security.test.ts) diff --git a/specs/archive/2026-08-16-supply-chain-vulnerability-remediation/proposal.md b/specs/archive/2026-08-16-supply-chain-vulnerability-remediation/proposal.md new file mode 100644 index 0000000..e58e3ae --- /dev/null +++ b/specs/archive/2026-08-16-supply-chain-vulnerability-remediation/proposal.md @@ -0,0 +1,47 @@ +# Proposal: Remediate renderer dependency vulnerabilities and licenses + +## Outcome + +Upgrade the four direct chart-rendering packages to non-vulnerable major versions, preserve +the documented chart contract and offline security boundary, and make the release license +gate pass only after maintainers explicitly select the compatible license branch for every +compound or metadata-incomplete transitive package. + +## Context + +The Phase 0 release audit currently reports eight known vulnerabilities: seven high and one +moderate. The fix paths require ECharts 6 and the Vega 6 / Vega-Lite 6 / Vega-Embed 7 family, +so this is a public-compatibility and security change rather than a routine lockfile refresh. +The license gate also refuses `khroma`, `dompurify`, and `json-schema` until the exact installed +license text and the project's selected branch are recorded. Release CI already blocks on +these gates; this packet defines the remediation needed to remove that block without weakening +the checks. + +## Scope + +- In scope: upgrade `echarts` to 6.1.0, `vega` to 6.4.0, `vega-lite` to 6.4.3, and + `vega-embed` to 7.1.0 or newer compatible releases on those major lines; retain interpreted + Vega expressions; test chart compatibility and malicious payloads; select and record the + MIT branch for `khroma`, Apache-2.0 for `dompurify`, and BSD-3-Clause for `json-schema` only + after human approval; update the lockfile, current specs, audit evidence, and release gates. +- Out of scope: adding chart engines, weakening CSP, enabling `unsafe-eval`, redesigning raw + HTML handling, publishing a package, changing trusted-publisher settings, hiding or waiving + advisories, or claiming untested platform support. + +## Risks and rollback + +- Risk: major renderer releases can alter schemas, defaults, generated markup, accessibility, + runtime size, or browser compatibility. Choosing the wrong license branch can also create an + incompatible distribution obligation. +- Rollback: revert the dependency and lockfile changes while retaining the failing release + gate and all audit evidence. Do not release the vulnerable dependency line as supported to + recover from a visual regression. + +## Validation plan + +The change is valid only when the exact packed candidate has no undisposed known vulnerability +or license finding, existing and adversarial chart fixtures pass under the strict CSP without +view-time network access, representative charts render through their documented real surfaces, +and already-created portable pages remain usable after the package is removed. Evidence must +include the before/after audit, exact license-file hashes and approved branches, package and +runtime-size deltas, browser smoke results, and regression tests for the cited payload classes. diff --git a/specs/archive/2026-08-16-supply-chain-vulnerability-remediation/tasks.md b/specs/archive/2026-08-16-supply-chain-vulnerability-remediation/tasks.md new file mode 100644 index 0000000..f024f56 --- /dev/null +++ b/specs/archive/2026-08-16-supply-chain-vulnerability-remediation/tasks.md @@ -0,0 +1,14 @@ +# Tasks: Remediate renderer dependency vulnerabilities and licenses + +- [x] Confirm proposal validation and human approval. +- [x] Record approved license branches and bind them to exact installed license-file hashes. +- [x] Upgrade the ECharts and Vega renderer family together and update the lockfile. +- [x] Add compatibility coverage for every documented chart kind and canonical example. +- [x] Add adversarial ECharts tooltip and Vega expression, data-mutation, and global-gadget tests. +- [x] Verify strict CSP, interpreter-only Vega execution, no view-time network, and no new globals. +- [x] Verify representative browser rendering, accessibility behavior, and runtime/package-size deltas. +- [x] Verify old self-contained pages remain usable after package removal. +- [x] Generate exact-candidate audit, license, signature, SBOM, provenance, and integrity evidence. +- [x] Update `specs/current/governance-policy.spec.md` and add + `specs/current/renderer-dependencies.spec.md`. +- [x] Reconcile documentation, run repository validation, verify all evidence, and archive the packet. diff --git a/specs/current/contract-integrity.spec.md b/specs/current/contract-integrity.spec.md new file mode 100644 index 0000000..641e6b1 --- /dev/null +++ b/specs/current/contract-integrity.spec.md @@ -0,0 +1,21 @@ +# Contract documentation integrity + +## Local validation + +- `QUAL-08`: `npm run check:links` scans Markdown contract surfaces under `README.md`, + `docs/`, and non-template `specs/` records. +- Relative file and heading targets resolve from their Markdown source. Typed packet evidence + links (`@test`, `@manual`, and `@model`) resolve from repository root according to the spec + workflow convention. +- Missing paths, repository escapes, malformed percent encoding, and absent Markdown headings + fail with source path, line, target, and failure class. Fenced and inline code are not links. +- The deterministic local check is registered as `docs-link-integrity` in `npm run check`. + +## Official sources + +- `npm run check:links -- --external` separately probes unique links on the declared official + documentation hosts with a ten-second per-request timeout and followed redirects. +- HTTP success and redirects pass. Terminal failures, transient HTTP failures, request errors, + and timeouts are distinguished and cause a non-zero exit. +- Omitting `--external` prints an explicit skipped result. Local validation never treats a + skipped or failed external run as an official-source pass. diff --git a/specs/current/governance-policy.spec.md b/specs/current/governance-policy.spec.md new file mode 100644 index 0000000..89e2411 --- /dev/null +++ b/specs/current/governance-policy.spec.md @@ -0,0 +1,51 @@ +# Governance policy and current evidence + +## Canonical records + +- `docs/governance-policy.json` is the machine-validated status record. The canonical prose + policies are `docs/support-policy.md`, `docs/security.md`, `docs/data-governance.md`, + `docs/threat-model.md`, and public `SECURITY.md`. +- Missing, separately scoped, or failed evidence cannot become supported/readiness state. + The deterministic repository check enforces required platform cells, data modes, threat + boundaries, provider prerequisites, Node floor, and high-level README claim consistency. + +## Current decisions + +- `COMPAT-01`: Node 24 is the support floor. Target coverage is Ubuntu 24.04 LTS, current and + previous macOS, Windows 11 native/WSL, current and oldest-supported stable OpenCode 1.x, + latest-two desktop Chromium/Firefox/Safari and mobile Android Chrome/iOS Safari where + available. An exact Ubuntu/Node/OpenCode/single-Chromium packed-candidate observation is + tested but incomplete. There are zero supported complete cells; all target cells are + unverified. Public preview may expose that state but cannot promote the observation or claim + support; certification still requires the target matrix. +- `SEC-01`: current, partial, and planned trust boundaries have owners, threats, controls, + tests, residual risk and review triggers. Planned identity/connectors do not become shipped. +- `SEC-10`: private GitHub vulnerability reporting is enabled and provider-verified, so the + public security policy directs sensitive reports to the private advisory path and refuses + sensitive public issues. Severity/response, rotation and compromised-release processes are + published; release-level response targets still require a named accountable owner. +- `PRIV-01`–`PRIV-07`: the mode inventory and governance policy separate local, loopback, + user-operated public targets, planned authenticated/connectors, journey studies and release + evidence. Local creation has no default project telemetry. Provider-wide erasure, residency, + backups, SLA, legal compliance and project-operated hosting are not claimed. The + current publication/deployment paths scan content, frontmatter/title metadata, every + current top-level public file, provider target configuration, and reused staging trees + before audience expansion; a force override is invocation-scoped. The + redistribution inventory binds every retained binary asset to exact project provenance and + hash, verifies that no font files are embedded, and keeps external benchmark media link-only. +- `DIST-03`–`DIST-07`: only the current package minor receives fixes; deprecations receive at + least one supported-release notice except active exploits. D-06 selects npm trusted + publishing/provenance, npm CycloneDX, audit/signatures, license disposition and exact packed- + byte binding. The dated provider report verifies the exact trusted publisher and published + 0.14.4 registry integrity, signature, tag/commit/workflow-bound SLSA provenance. The approved renderer + remediation clears the current audit and hash-bound license gates and adds exact candidate + SBOM/provenance artifact retention. Current source, documentation, example, asset, font, and + reference redistribution disposition also passes. Provider and platform evidence remains + incomplete, so no production-readiness or certified-level claim is permitted. An unsupported + public preview may distribute only through the closed pre-publish hard gates and becomes a + verified public preview only after registry integrity, signature, and provenance pass. The + tag workflow fails closed on prepublish evidence, publishes the coordinated tarball, and + verifies registry integrity/signature/provenance afterward. The first 0.14.4 post-publish + job exposed and retained a singleton-array parser failure; the corrected verifier passes the + retained response and independently identical registry tarball. Preview history supplies no + certification waiver. diff --git a/specs/current/journey-evidence.spec.md b/specs/current/journey-evidence.spec.md new file mode 100644 index 0000000..690524c --- /dev/null +++ b/specs/current/journey-evidence.spec.md @@ -0,0 +1,38 @@ +# Journey corpus and outcome evidence + +## Versioned corpus + +- `UX-01`: `docs/journeys/corpus.json` owns a machine-validated create/revise/review/share + corpus, explicit decision state, synthetic comprehension fixtures, and a visible deferred + record for reconnect/export/archive/restore. The deferred workflows keep `UX-01` partial. +- Each comprehension fixture declares synthetic provenance and an exact four-field rubric. + Fixture pages are inputs, not participant evidence. + +## Private study records + +- `OUT-05`: real records require affirmative versioned consent, pseudonymous participant IDs, + categorical eligibility/conflict data, bounded timings, exact release/platform fields, and + withdrawal state. Unknown fields (including direct identity fields) fail validation. +- Raw answers and participant codes remain access-controlled and gitignored. Aggregation omits + both. Synthetic, withdrawn, conflicted, nonrepresentative, and secondary records never enter + the Phase 0 primary-user denominator. +- The approved protocol in `docs/journeys/README.md` prohibits default telemetry and specifies + data minimization, access, withdrawal, and raw-record deletion. Declining participation does + not affect product functionality. + +## Acceptance semantics + +- `OUT-02`: each platform ID claimed by a study needs at least one eligible first-time-user + pass: exact release, README only, no checkout/account/assistance, and create/reopen in at most + 600 seconds. An untested claimed platform is `incomplete`; an observed covered miss is + `fail`; no claimed platforms can never pass. +- `OUT-03`: at least ten eligible representative primary participants are required and every + participant must answer all four rubric fields. A pass requires at least 90% to score all + four true without assistance in at most 60 seconds. Fewer than ten is `incomplete`, not a + pass or a rounded rate. +- The retained status at `docs/evidence/journeys/phase-0-baseline-status.md` records zero real + participants. Therefore `OUT-02` and `OUT-03` remain incomplete and cannot support a human- + usability, platform-support, or certified-level claim. Their absence does not block an + explicitly unsupported and uncertified public preview whose separate hard gates pass. + Synthetic Kimi/model probes may diagnose the instructions or corpus but never change this + status or enter the participant denominator. diff --git a/specs/current/renderer-dependencies.spec.md b/specs/current/renderer-dependencies.spec.md new file mode 100644 index 0000000..24d0f64 --- /dev/null +++ b/specs/current/renderer-dependencies.spec.md @@ -0,0 +1,32 @@ +# Renderer dependency security and compatibility + +## Current behavior + +- `RENDER-02`: every checked-in example resolves through the approved patched ECharts and + Vega renderer majors. Vega-Lite is compiled to Vega; malformed specifications become inline + actionable errors. The intentionally malformed incident example remains explicitly expected. +- `SEC-02`: chart payload JSON is untrusted and script-context escaped. The exact candidate + tests ECharts built-in tooltip encoding plus the cited Vega global-gadget and data-mutation + payload classes. +- `SEC-04`: portable pages retain the strict on-disk CSP, make no view-time requests, and run + Vega through its AST interpreter without `unsafe-eval`. The application stores neither a + Vega View nor a debug gadget global. +- `DIST-04`: CI audits the clean lockfile, validates exact license dispositions, creates an + SBOM and candidate provenance statement, packs once, and binds retained evidence to the + candidate tarball. Release CI additionally verifies registry signature and provenance after + publishing those same bytes. +- `DIST-05`: the renderer family is lockfile-pinned and its license, CSP/network, browser + weight, update, and removal impacts are dated. Three exceptional license branches are bound + to approved versions and exact file hashes. Already-generated pages remain functional after + the installed package tree is removed. +- `QUAL-06`: deterministic tests and a real-browser smoke cover tooltip, expression/global, + data-mutation, script-context, CSP, offline, resource, and candidate-gate boundaries. + +## Evidence boundary + +- `docs/evidence/governance/renderer-remediation-2026-08-16.md` records exact versions, hashes, + package/SBOM/audit/signature results, browser observations, failures, and exclusions. +- `docs/license-dispositions.json` is the machine-readable maintainer decision. A version, + branch, path, or hash change fails closed and needs new review. +- This evidence does not promote a supported OS/browser cell or prove an unpublished future + registry attestation. diff --git a/src/cli.ts b/src/cli.ts index aca4188..9153557 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -168,6 +168,7 @@ async function deployCommand(args: string[]): Promise { const publisher = new CloudflarePublisher(dir, { workerName: name, stagingDir: join(home, ".cache", "opencode-artifacts", "cloudflare", name), + allowSensitive: args.includes("--force"), }); const url = await publisher.deploy(); console.log(url ?? "deployed (workers.dev url not found in output)"); @@ -178,7 +179,12 @@ async function deployCommand(args: string[]): Promise { const branch = optionValue(args, "--branch") ?? "main"; if (!repo || !repo.includes("/")) usage(); const cloneDir = join(home, ".cache", "opencode-artifacts", "ghpages", repo.replace("/", "__")); - const publisher = new GitHubPagesPublisher(dir, { repo, branch, cloneDir }); + const publisher = new GitHubPagesPublisher(dir, { + repo, + branch, + cloneDir, + allowSensitive: args.includes("--force"), + }); const baseUrl = await publisher.sync("deploy artifacts"); console.log(baseUrl); } diff --git a/src/cloudflare-publisher.ts b/src/cloudflare-publisher.ts index 9efaf17..46e41b4 100644 --- a/src/cloudflare-publisher.ts +++ b/src/cloudflare-publisher.ts @@ -3,11 +3,13 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { FilePublisher, type PublishInput, type PublishResult, type Publisher } from "./publisher.ts"; import { copyArtifacts, type Runner, runProcess } from "./github-pages.ts"; +import { assertSafeDeployment } from "./guard.ts"; export interface CloudflareOptions { workerName: string; stagingDir: string; runner?: Runner; + allowSensitive?: boolean; } const WRANGLER_TOML = (name: string, main: string, kvId: string) => `name = "${name}" @@ -35,6 +37,7 @@ export class CloudflarePublisher implements Publisher { private readonly workerName: string; private readonly stagingDir: string; private readonly runner: Runner; + private readonly allowSensitive: boolean; constructor(localDir: string, options: CloudflareOptions) { this.local = new FilePublisher(localDir); @@ -42,6 +45,7 @@ export class CloudflarePublisher implements Publisher { this.workerName = options.workerName; this.stagingDir = options.stagingDir; this.runner = options.runner ?? runProcess; + this.allowSensitive = options.allowSensitive ?? false; } async publish(input: PublishInput): Promise { @@ -51,7 +55,17 @@ export class CloudflarePublisher implements Publisher { } async deploy(): Promise { + await assertSafeDeployment( + this.localDir, + `workerName=${this.workerName}`, + this.allowSensitive, + ); const main = await this.stage(); + await assertSafeDeployment( + join(this.stagingDir, "assets"), + `workerName=${this.workerName}`, + this.allowSensitive, + ); const kvId = await this.ensureKvNamespace(); await writeFile( join(this.stagingDir, "wrangler.toml"), diff --git a/src/github-pages.ts b/src/github-pages.ts index 708c99c..38b9a50 100644 --- a/src/github-pages.ts +++ b/src/github-pages.ts @@ -3,6 +3,7 @@ import { cp, mkdir, readdir } from "node:fs/promises"; import { join } from "node:path"; import { promisify } from "node:util"; import { FilePublisher, type PublishInput, type PublishResult, type Publisher } from "./publisher.ts"; +import { assertSafeDeployment } from "./guard.ts"; export type Runner = (command: string, args: string[], cwd?: string) => Promise; @@ -20,6 +21,7 @@ export interface GitHubPagesOptions { branch?: string; cloneDir: string; runner?: Runner; + allowSensitive?: boolean; } export function pagesBaseUrl(repo: string): string { @@ -45,6 +47,7 @@ export class GitHubPagesPublisher implements Publisher { private readonly branch: string; private readonly cloneDir: string; private readonly runner: Runner; + private readonly allowSensitive: boolean; constructor(localDir: string, options: GitHubPagesOptions) { this.local = new FilePublisher(localDir); @@ -53,6 +56,7 @@ export class GitHubPagesPublisher implements Publisher { this.branch = options.branch ?? "main"; this.cloneDir = options.cloneDir; this.runner = options.runner ?? runProcess; + this.allowSensitive = options.allowSensitive ?? false; } async publish(input: PublishInput): Promise { @@ -62,9 +66,19 @@ export class GitHubPagesPublisher implements Publisher { } async sync(commitMessage: string): Promise { + await assertSafeDeployment( + this.localDir, + `repository=${this.repo}\nbranch=${this.branch}`, + this.allowSensitive, + ); await this.ensureClone(); await this.runner("git", ["-C", this.cloneDir, "pull", "--ff-only"]).catch(() => {}); await copyArtifacts(this.localDir, this.cloneDir); + await assertSafeDeployment( + this.cloneDir, + `repository=${this.repo}\nbranch=${this.branch}`, + this.allowSensitive, + ); await this.runner("git", ["-C", this.cloneDir, "add", "-A"]); const status = await this.runner("git", ["-C", this.cloneDir, "status", "--porcelain"]); if (status.trim() !== "") { diff --git a/src/guard.ts b/src/guard.ts index e256a77..b3ade8b 100644 --- a/src/guard.ts +++ b/src/guard.ts @@ -39,9 +39,29 @@ export interface SensitiveFileFinding { export async function scanArtifactDirectory(dir: string): Promise { const results: SensitiveFileFinding[] = []; for (const entry of await readdir(dir, { withFileTypes: true })) { - if (!entry.isFile() || !entry.name.endsWith(".html")) continue; + if (!entry.isFile()) continue; const findings = scanSensitive(await readFile(join(dir, entry.name), "utf8")); if (findings.length > 0) results.push({ file: entry.name, findings }); } return results; } + +export async function assertSafeDeployment( + dir: string, + configuration: string, + allowSensitive = false, +): Promise { + if (allowSensitive) return; + const results = await scanArtifactDirectory(dir); + const configurationFindings = scanSensitive(configuration); + if (configurationFindings.length > 0) { + results.push({ file: "", findings: configurationFindings }); + } + if (results.length === 0) return; + const details = results + .map(({ file, findings }) => `${file}: ${formatFindings(findings)}`) + .join("; "); + throw new Error( + `deploy blocked: credential-looking strings found: ${details}. Re-run with --force to deploy anyway.`, + ); +} diff --git a/src/plugin.ts b/src/plugin.ts index 4a29c0e..e686b3d 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -155,12 +155,14 @@ export const ArtifactsPlugin: Plugin = async (_input, options) => { repo: resolved.repo, branch: resolved.branch, cloneDir: ghPagesCloneDir(resolved.repo), + allowSensitive: args.force === true, }); } if (resolved.target === "cloudflare" && resolved.workerName) { return new CloudflarePublisher(localDir, { workerName: resolved.workerName, stagingDir: cfStagingDir(resolved.workerName), + allowSensitive: args.force === true, }); } throw new Error( diff --git a/src/render.ts b/src/render.ts index 19eecf7..6003c22 100644 --- a/src/render.ts +++ b/src/render.ts @@ -270,7 +270,9 @@ const BOOT = `(function () { if (entry.error) { fail(entry.error); return; } try { if (entry.kind === "vega") { - window.vegaEmbed(el, entry.spec, { actions: false, ast: true }); + Promise.resolve(window.vegaEmbed(el, entry.spec, { actions: false, ast: true })).catch(function (err) { + fail(err && err.message ? err.message : String(err)); + }); } else if (entry.kind === "echarts") { var chart = window.echarts.init(el); chart.setOption(entry.spec); diff --git a/test/cli.test.ts b/test/cli.test.ts index c7f055a..88cab16 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -36,3 +36,23 @@ test("deploy scans existing artifact files before invoking a host", async () => await rm(dir, { recursive: true, force: true }); } }); + +test("deploy scans provider configuration before invoking a host", async () => { + const dir = await mkdtemp(join(tmpdir(), "cli-deploy-config-")); + try { + await writeFile(join(dir, "clean.html"), "

clean

"); + await assert.rejects( + run(process.execPath, [ + CLI, + "deploy", + "--dir", + dir, + "--repo", + "owner/ghp_0123456789abcdefABCDEF0123456789", + ]), + (err: Error & { stderr?: string }) => /deploy blocked.*deployment-config/.test(err.stderr ?? ""), + ); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/test/cloudflare.test.ts b/test/cloudflare.test.ts index 6174dcb..26e955b 100644 --- a/test/cloudflare.test.ts +++ b/test/cloudflare.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { handleApiRequest, type KVStore } from "../src/cloudflare/handler.ts"; @@ -160,3 +160,31 @@ test("cloudflare publishers use worker-specific KV namespace titles", async () = await rm(dir, { recursive: true, force: true }); } }); + +test("cloudflare deploy blocks sensitive stale files in the reused staging tree", async () => { + const dir = await mkdtemp(join(tmpdir(), "cf-stale-")); + try { + const localDir = join(dir, "local"); + const stagingDir = join(dir, "staging"); + await mkdir(localDir); + await mkdir(join(stagingDir, "assets"), { recursive: true }); + await writeFile(join(localDir, "clean.html"), "

clean

"); + await writeFile( + join(stagingDir, "assets", "stale.json"), + "ghp_0123456789abcdefABCDEF0123456789", + ); + const calls: string[] = []; + const publisher = new CloudflarePublisher(localDir, { + workerName: "opencode-artifacts", + stagingDir, + runner: async (command, args) => { + calls.push(`${command} ${args.join(" ")}`); + return ""; + }, + }); + await assert.rejects(publisher.deploy(), /deploy blocked.*stale\.json/); + assert.deepEqual(calls, []); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/test/documentation-links.test.ts b/test/documentation-links.test.ts new file mode 100644 index 0000000..1131f35 --- /dev/null +++ b/test/documentation-links.test.ts @@ -0,0 +1,108 @@ +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + collectDocumentationLinks, + extractDocumentationLinks, + markdownAnchors, + probeOfficialLinks, + validateLocalDocumentationLinks, +} from "../scripts/documentation-links.ts"; + +async function fixture(files: Record): Promise { + const root = await mkdtemp(join(tmpdir(), "artifact-doc-links-")); + for (const [path, content] of Object.entries(files)) { + const absolute = join(root, path); + await mkdir(dirname(absolute), { recursive: true }); + await writeFile(absolute, content, "utf8"); + } + return root; +} + +test("local documentation links resolve files and duplicate GitHub-style anchors", async () => { + const root = await fixture({ + "README.md": "[first](docs/guide.md#hello-world) [second](docs/guide.md#hello-world-1) [encoded](docs/with%20space.md#encoded-heading)\n", + "docs/guide.md": "# Hello, world!\n\n## Hello, world!\n", + "docs/with space.md": "# Encoded heading\n", + }); + try { + assert.deepEqual(await validateLocalDocumentationLinks(root), []); + assert.deepEqual([...markdownAnchors("# A!\n## A!\n")], ["a", "a-1"]); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("local documentation validation reports missing paths, anchors, and escapes with source lines", async () => { + const root = await fixture({ + "README.md": [ + "[missing](docs/nope.md)", + "[anchor](docs/guide.md#absent)", + "[escape](../outside.md)", + "[invalid](docs/%ZZ.md)", + ].join("\n"), + "docs/guide.md": "# Present\n", + }); + try { + const issues = await validateLocalDocumentationLinks(root); + assert.deepEqual(issues.map((issue) => [issue.line, issue.reason]), [ + [1, "missing-path"], + [2, "missing-anchor"], + [3, "path-escape"], + [4, "invalid-target"], + ]); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("link extraction ignores code and distinguishes official from other external links", () => { + const markdown = [ + "\`[inline](missing.md)\`", + "\`\`\`md", + "[fenced](missing.md)", + "\`\`\`", + "[OpenCode](https://opencode.ai/docs/plugins/)", + "[Example](https://example.com/)", + "[mail](mailto:security@example.com)", + ].join("\n"); + assert.deepEqual( + extractDocumentationLinks(markdown, "README.md").map((link) => [link.line, link.kind]), + [[5, "official"], [6, "external"]], + ); +}); + +test("official probing preserves pass, terminal, transient, and timeout outcomes", async () => { + const root = await fixture({ + "README.md": [ + "[ok](https://docs.github.com/ok)", + "[gone](https://docs.github.com/gone)", + "[busy](https://docs.github.com/busy)", + "[slow](https://docs.github.com/slow)", + ].join("\n"), + }); + try { + const links = await collectDocumentationLinks(root); + const results = await probeOfficialLinks(links, { + timeoutMs: 5, + fetcher: async (url, signal) => { + if (url.endsWith("/ok")) return { status: 200, url }; + if (url.endsWith("/gone")) return { status: 404, url }; + if (url.endsWith("/busy")) return { status: 503, url }; + return new Promise((_, reject) => { + signal.addEventListener("abort", () => reject(new Error("aborted")), { once: true }); + }); + }, + }); + assert.deepEqual(results.map((result) => result.status), [ + "transient-failure", + "terminal-failure", + "pass", + "timeout", + ]); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); diff --git a/test/github-pages.test.ts b/test/github-pages.test.ts index 2f1c7e9..6b2f3e7 100644 --- a/test/github-pages.test.ts +++ b/test/github-pages.test.ts @@ -75,6 +75,29 @@ test("local state directories are never published", async () => { await rm(dir, { recursive: true, force: true }); }); +test("sync blocks sensitive stale files already present in the destination clone", async () => { + const dir = await mkdtemp(join(tmpdir(), "ghpages-stale-")); + try { + const cloneDir = join(dir, "clone"); + const localDir = join(dir, "local"); + await mkdir(join(cloneDir, ".git"), { recursive: true }); + await mkdir(localDir); + await writeFile(join(cloneDir, "stale.html"), "ghp_0123456789abcdefABCDEF0123456789"); + await writeFile(join(localDir, "clean.html"), "

clean

"); + const calls: string[] = []; + const publisher = new GitHubPagesPublisher(localDir, { + repo: "bitgorust/artifacts", + cloneDir, + runner: fakeRunner(calls), + }); + await assert.rejects(publisher.sync("deploy"), /deploy blocked.*stale\.html/); + assert.ok(!calls.some((call) => call.includes("add -A"))); + assert.ok(!calls.some((call) => call.includes("push origin"))); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + test("a missing repo is created then cloned", async () => { const dir = await mkdtemp(join(tmpdir(), "ghpages-")); const calls: string[] = []; diff --git a/test/governance-policy.test.ts b/test/governance-policy.test.ts new file mode 100644 index 0000000..ef6bf0e --- /dev/null +++ b/test/governance-policy.test.ts @@ -0,0 +1,82 @@ +import { readFile } from "node:fs/promises"; +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + validateGovernanceClaims, + validateGovernancePolicy, + validateGovernanceRepository, + validateRedistributionInventory, +} from "../scripts/governance-policy.ts"; + +const root = new URL("..", import.meta.url).pathname; +const policy = JSON.parse(await readFile(new URL("../docs/governance-policy.json", import.meta.url), "utf8")) as Record; +const redistribution = JSON.parse( + await readFile(new URL("../docs/redistribution-inventory.json", import.meta.url), "utf8"), +) as Record; + +function inventoriedAssets(value: Record): Record { + return Object.fromEntries( + (value["binaryAssets"] as Array>).map((item) => [item["path"], item["sha256"]]), + ); +} + +test("checked-in governance policy is complete and claim-consistent", async () => { + assert.deepEqual(validateGovernancePolicy(policy), []); + assert.deepEqual(await validateGovernanceRepository(root), []); +}); + +test("support claims cannot pass without exact dated evidence", () => { + const changed = structuredClone(policy); + const cells = changed["supportCells"] as Array>; + cells[1]["status"] = "supported"; + const errors = validateGovernancePolicy(changed).join("\n"); + assert.match(errors, /cannot be supported without dated evidence/); +}); + +test("required target cells, data modes, and threat boundaries cannot disappear", () => { + const changed = structuredClone(policy); + (changed["supportCells"] as unknown[]).pop(); + (changed["dataInventory"] as unknown[]).pop(); + (changed["threatBoundaries"] as unknown[]).pop(); + const errors = validateGovernancePolicy(changed).join("\n"); + assert.match(errors, /supportCells is missing node-before-24/); + assert.match(errors, /dataInventory is missing release-evidence/); + assert.match(errors, /threatBoundaries is missing connectors/); +}); + +test("inflated README provenance and mismatched Node claims fail consistency", () => { + const errors = validateGovernanceClaims({ + readme: "with provenance attestations", + security: "", + support: "", + dataGovernance: "", + hosted: "", + packageJson: { engines: { node: ">=20" } }, + }).join("\n"); + assert.match(errors, /claims provenance attestations/); + assert.match(errors, /engines.node must match/); + assert.match(errors, /verified private reporting/); + assert.match(errors, /public-preview status/); + assert.match(errors, /separate public preview from certification/); +}); + +test("redistribution inventory binds every retained binary and keeps official references link-only", () => { + const actualAssets = inventoriedAssets(redistribution); + assert.deepEqual(validateRedistributionInventory(redistribution, actualAssets), []); + + const missing = structuredClone(redistribution); + (missing["binaryAssets"] as unknown[]).pop(); + assert.match(validateRedistributionInventory(missing, actualAssets).join("\n"), /binaryAssets is missing/); + + const changed = structuredClone(redistribution); + ((changed["binaryAssets"] as Array>)[0])["sha256"] = "a".repeat(64); + ((changed["externalBenchmarkReferences"] as Array>)[0])["localCopy"] = "copied.png"; + const changedErrors = validateRedistributionInventory(changed, actualAssets).join("\n"); + assert.match(changedErrors, /sha256 does not match retained bytes/); + assert.match(changedErrors, /localCopy must be null/); + + const withFont = { ...actualAssets, "docs/fonts/unreviewed.woff2": "b".repeat(64) }; + const fontErrors = validateRedistributionInventory(redistribution, withFont).join("\n"); + assert.match(fontErrors, /binaryAssets is missing docs\/fonts\/unreviewed\.woff2/); + assert.match(fontErrors, /embeds an undisposed font/); +}); diff --git a/test/guard.test.ts b/test/guard.test.ts index 7f313b6..96f2b1c 100644 --- a/test/guard.test.ts +++ b/test/guard.test.ts @@ -3,7 +3,12 @@ import assert from "node:assert/strict"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { formatFindings, scanArtifactDirectory, scanSensitive } from "../src/guard.ts"; +import { + assertSafeDeployment, + formatFindings, + scanArtifactDirectory, + scanSensitive, +} from "../src/guard.ts"; test("clean content produces no findings", () => { assert.deepEqual(scanSensitive("# Report\n\nAll values here are fake."), []); @@ -34,14 +39,26 @@ test("formatFindings redacts the matched secret", () => { assert.ok(!text.includes("AKIAIOSFODNN7EXAMPLE")); }); -test("deploy scanning checks every HTML artifact in a directory", async () => { +test("deploy scanning checks every staged file in an artifact directory", async () => { const dir = await mkdtemp(join(tmpdir(), "guard-")); try { await writeFile(join(dir, "clean.html"), "

clean

"); await writeFile(join(dir, "copied.html"), "ghp_0123456789abcdefABCDEF0123456789"); - await writeFile(join(dir, "ignored.json"), "ghp_0123456789abcdefABCDEF0123456789"); + await writeFile(join(dir, "manifest.json"), "ghp_0123456789abcdefABCDEF0123456789"); const results = await scanArtifactDirectory(dir); - assert.deepEqual(results.map((result) => result.file), ["copied.html"]); + assert.deepEqual(results.map((result) => result.file), ["copied.html", "manifest.json"]); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + +test("deployment scanning covers provider configuration and only a targeted override bypasses it", async () => { + const dir = await mkdtemp(join(tmpdir(), "guard-deploy-")); + try { + await writeFile(join(dir, "clean.html"), "

clean

"); + const configuration = "repository=owner/ghp_0123456789abcdefABCDEF0123456789"; + await assert.rejects(assertSafeDeployment(dir, configuration), /deploy blocked.*deployment-config/); + await assert.doesNotReject(assertSafeDeployment(dir, configuration, true)); } finally { await rm(dir, { recursive: true, force: true }); } diff --git a/test/journey-study.test.ts b/test/journey-study.test.ts new file mode 100644 index 0000000..8267147 --- /dev/null +++ b/test/journey-study.test.ts @@ -0,0 +1,163 @@ +import { readFile } from "node:fs/promises"; +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + summarizeJourneyStudy, + validateJourneyCorpus, + validateJourneyStudy, +} from "../scripts/journey-study-lib.ts"; + +const corpus = JSON.parse(await readFile(new URL("../docs/journeys/corpus.json", import.meta.url), "utf8")) as unknown; + +function record(index: number, options: { + pass?: boolean; + synthetic?: boolean; + platformId?: string; + withdrawn?: boolean; + conflict?: string; +} = {}): Record { + const pass = options.pass ?? true; + const synthetic = options.synthetic ?? false; + return { + participantId: synthetic ? `synthetic-${index}` : `p-${String(index).padStart(6, "0")}`, + participantRole: "primary", + representative: true, + firstTimeUser: true, + conflict: options.conflict ?? "none", + synthetic, + consent: synthetic ? null : { + given: true, + at: "2026-08-16T08:00:00Z", + protocolVersion: 1, + withdrawnAt: options.withdrawn ? "2026-08-16T09:00:00Z" : null, + }, + platform: { + id: options.platformId ?? "ubuntu-chromium", + os: "Ubuntu", + osVersion: "24.04", + nodeVersion: "24.6.0", + opencodeVersion: "1.18.18", + browser: "Chromium", + browserVersion: "139", + }, + firstUse: { + startedAt: "2026-08-16T08:01:00Z", + endedAt: pass ? "2026-08-16T08:09:00Z" : "2026-08-16T08:12:00Z", + elapsedSeconds: pass ? 480 : 660, + completed: pass, + readmeOnly: true, + repositoryCheckout: false, + hostingAccount: false, + maintainerAssistance: false, + failureStep: pass ? null : "render", + }, + comprehension: { + fixtureId: ["incident", "release", "review", "migration"][index % 4], + artifactSha256: "a".repeat(64), + startedAt: "2026-08-16T08:15:00Z", + elapsedSeconds: pass ? 45 : 61, + maintainerAssistance: false, + answers: { + purpose: "synthetic fixture purpose", + primaryFinding: "synthetic fixture finding", + provenance: "synthetic fixture provenance", + nextAction: "synthetic fixture next action", + }, + scores: { + purpose: pass, + primaryFinding: pass, + provenance: pass, + nextAction: pass, + }, + }, + }; +} + +function study(records: Record[], claimedPlatformIds = ["ubuntu-chromium"]): Record { + return { + schemaVersion: 1, + studyId: "phase-0-2026-08-16", + corpusVersion: 1, + release: { + package: "opencode-artifacts", + version: "0.14.3", + integrity: "sha512-abc123=", + }, + claimedPlatformIds, + records, + }; +} + +test("checked-in corpus covers create, revise, review, and share with valid rubrics", () => { + assert.deepEqual(validateJourneyCorpus(corpus), []); +}); + +test("nine of ten eligible participants and every claimed platform pass exact thresholds", () => { + const records = Array.from({ length: 10 }, (_, index) => record(index + 1, { pass: index !== 9 })); + records.push(record(20, { synthetic: true, pass: false })); + const summary = summarizeJourneyStudy(study(records), corpus); + assert.equal(summary.firstUse.status, "pass"); + assert.equal(summary.comprehension.status, "pass"); + assert.equal(summary.comprehension.eligibleParticipants, 10); + assert.equal(summary.comprehension.passingParticipants, 9); + assert.equal(summary.comprehension.passRate, 0.9); + assert.equal(summary.excluded.synthetic, 1); +}); + +test("missing runs and uncovered platforms remain incomplete, while observed misses fail", () => { + const empty = summarizeJourneyStudy(study([], []), corpus); + assert.equal(empty.firstUse.status, "incomplete"); + assert.equal(empty.comprehension.status, "incomplete"); + assert.equal(empty.comprehension.passRate, null); + + const uncovered = summarizeJourneyStudy(study([record(1)], ["ubuntu-chromium", "macos-safari"]), corpus); + assert.equal(uncovered.firstUse.status, "incomplete"); + assert.deepEqual(uncovered.firstUse.missingPlatformIds, ["macos-safari"]); + + const observedFailure = summarizeJourneyStudy(study([record(1, { pass: false })]), corpus); + assert.equal(observedFailure.firstUse.status, "fail"); +}); + +test("withdrawn, conflicted, secondary, nonrepresentative, and synthetic records are excluded", () => { + const excluded = [ + record(1, { withdrawn: true }), + record(2, { conflict: "contributor" }), + { ...record(3), participantRole: "secondary" }, + { ...record(4), representative: false }, + record(5, { synthetic: true }), + ]; + const summary = summarizeJourneyStudy(study(excluded), corpus); + assert.equal(summary.comprehension.eligibleParticipants, 0); + assert.deepEqual(summary.excluded, { + synthetic: 1, + withdrawn: 1, + conflict: 1, + nonRepresentative: 1, + secondary: 1, + }); +}); + +test("validation rejects identity fields, absent consent, unknown fixtures, bad timing, and duplicates", () => { + const first = record(1); + first["email"] = "must-not-be-collected@example.com"; + first["consent"] = null; + const comprehension = first["comprehension"] as Record; + comprehension["fixtureId"] = "unknown"; + comprehension["artifactSha256"] = "bad"; + const firstUse = first["firstUse"] as Record; + firstUse["elapsedSeconds"] = 10; + const errors = validateJourneyStudy(study([first, record(1)]), corpus).join("\n"); + assert.match(errors, /unexpected field email/); + assert.match(errors, /must be an object for real records/); + assert.match(errors, /not in the approved corpus/); + assert.match(errors, /64 lowercase hexadecimal/); + assert.match(errors, /must match the bounded timestamps/); + assert.match(errors, /participantId is duplicated/); +}); + +test("summary output redacts participant IDs and answer text", () => { + const source = study([record(123456)]); + const summary = JSON.stringify(summarizeJourneyStudy(source, corpus)); + assert.doesNotMatch(summary, /p-123456/); + assert.doesNotMatch(summary, /synthetic fixture purpose/); +}); diff --git a/test/plugin.test.ts b/test/plugin.test.ts index feee3f6..db5ca41 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -104,6 +104,38 @@ test("artifact_publish scans a title override for sensitive content", async () = }); }); +test("artifact_publish scans frontmatter metadata before writing manifests or galleries", async () => { + const publish = (await ArtifactsPlugin({} as unknown as PluginInput)).tool?.artifact_publish; + assert.ok(publish); + await withWorktree(async (dir) => { + const ctx: ToolContext = { + sessionID: "s1", + messageID: "m1", + agent: "test", + directory: dir, + worktree: dir, + abort: new AbortController().signal, + metadata: () => {}, + ask: async () => {}, + }; + const result = await publish.execute( + { + markdown: [ + "---", + "title: Clean title", + "description: ghp_0123456789abcdefABCDEF0123456789", + "source: synthetic", + "---", + "# Clean body", + ].join("\n"), + }, + ctx, + ); + assert.match(String(result), /Publish blocked/); + await assert.rejects(readFile(join(dir, ".opencode", "artifacts", "manifest.json"), "utf8")); + }); +}); + test("proactive option injects the guidance into the system transform", async () => { const off = await ArtifactsPlugin({} as unknown as PluginInput); assert.equal(off["experimental.chat.system.transform"], undefined); diff --git a/test/release-integrity.test.ts b/test/release-integrity.test.ts new file mode 100644 index 0000000..64147ea --- /dev/null +++ b/test/release-integrity.test.ts @@ -0,0 +1,263 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + candidateProvenance, + createLicenseInventory, + licenseInventoryErrors, + packCoordinate, + PREVIEW_POSTPUBLISH_GATES, + PREVIEW_PREPUBLISH_GATES, + releaseCandidateGateFailures, + releaseTransitionFailures, + validateLicenseDispositions, + verifyPublishedDistribution, + verifyTagVersion, + type ReleaseTransitionChecks, +} from "../scripts/release-integrity.ts"; + +test("license inventory reports missing and compound dispositions", () => { + const inventory = createLicenseInventory({ + packages: { + "": { license: "MIT" }, + "node_modules/a": { license: "MIT" }, + "node_modules/b": { license: "(MPL-2.0 OR Apache-2.0)" }, + "node_modules/c": {}, + }, + }); + assert.deepEqual(inventory.licenses, { MIT: 1, "(MPL-2.0 OR Apache-2.0)": 1, MISSING: 1 }); + assert.deepEqual(inventory.missingLicenseEntries, ["node_modules/c"]); + assert.equal(inventory.reviewRequired.length, 1); + assert.equal(licenseInventoryErrors(inventory).length, 2); +}); + +test("installed manifest metadata fills lockfile license omissions", () => { + const inventory = createLicenseInventory( + { packages: { "": { license: "MIT" }, "node_modules/a": { version: "1.0.0" } } }, + { "node_modules/a": "BSD-3-Clause" }, + ); + assert.deepEqual(inventory.licenses, { "BSD-3-Clause": 1 }); + assert.deepEqual(licenseInventoryErrors(inventory), []); +}); + +test("exact hash-bound license dispositions clear only their reviewed findings", () => { + const lockfile = { + packages: { + "": { license: "MIT" }, + "node_modules/missing": { version: "1.0.0" }, + "node_modules/choice": { version: "2.0.0", license: "(MPL-2.0 OR Apache-2.0)" }, + }, + }; + const value = { + schemaVersion: 1, + reviewedAt: "2026-08-16", + approvedBy: "maintainer", + approvedAt: "2026-08-16T14:57:27Z", + dispositions: [ + { + path: "node_modules/missing", + version: "1.0.0", + declaredLicense: null, + selectedLicense: "MIT", + licenseFile: "node_modules/missing/LICENSE", + sha256: "a".repeat(64), + }, + { + path: "node_modules/choice", + version: "2.0.0", + declaredLicense: "(MPL-2.0 OR Apache-2.0)", + selectedLicense: "Apache-2.0", + licenseFile: "node_modules/choice/LICENSE-APACHE", + sha256: "b".repeat(64), + }, + ], + }; + const inventory = createLicenseInventory(lockfile); + const validation = validateLicenseDispositions(lockfile, {}, value, { + "node_modules/missing/LICENSE": "a".repeat(64), + "node_modules/choice/LICENSE-APACHE": "b".repeat(64), + }); + assert.deepEqual(validation.errors, []); + assert.deepEqual([...validation.approvedPaths], ["node_modules/missing", "node_modules/choice"]); + assert.deepEqual(licenseInventoryErrors(inventory, validation.approvedPaths), []); + + const changed = structuredClone(value); + changed.dispositions[1].version = "2.0.1"; + changed.dispositions[1].selectedLicense = "GPL-3.0"; + changed.dispositions[1].sha256 = "c".repeat(64); + const errors = validateLicenseDispositions(lockfile, {}, changed, { + "node_modules/missing/LICENSE": "a".repeat(64), + "node_modules/choice/LICENSE-APACHE": "b".repeat(64), + }).errors.join("\n"); + assert.match(errors, /version does not match/); + assert.match(errors, /selectedLicense is not a branch/); + assert.match(errors, /sha256 does not match/); +}); + +test("release candidate property model blocks every failed constituent gate", () => { + const passing = { + audit: true, + licenses: true, + csp: true, + offline: true, + adversarialPayloads: true, + compatibility: true, + packedBytes: true, + }; + assert.deepEqual(releaseCandidateGateFailures(passing), []); + for (const name of Object.keys(passing) as Array) { + assert.deepEqual(releaseCandidateGateFailures({ ...passing, [name]: false }), [name]); + } +}); + +function previewChecks(): ReleaseTransitionChecks { + return { + hardGates: Object.fromEntries( + [...PREVIEW_PREPUBLISH_GATES, ...PREVIEW_POSTPUBLISH_GATES].map((gate) => [gate, true]), + ) as ReleaseTransitionChecks["hardGates"], + previewLabel: true, + unsupportedDisclosure: true, + missingEvidenceVisible: true, + certificationClaim: false, + out02: "incomplete", + out03: "incomplete", + support: "unverified", + }; +} + +test("public preview state machine permits visible missing certification evidence but no hard-gate failure", () => { + const base = previewChecks(); + assert.deepEqual(releaseTransitionFailures("preview-candidate", base), []); + assert.deepEqual(releaseTransitionFailures("public-preview", base), []); + + for (const gate of PREVIEW_PREPUBLISH_GATES) { + const changed = structuredClone(base); + changed.hardGates[gate] = false; + assert.match(releaseTransitionFailures("preview-candidate", changed).join("\n"), new RegExp(gate)); + assert.match(releaseTransitionFailures("public-preview", changed).join("\n"), new RegExp(gate)); + } + for (const gate of PREVIEW_POSTPUBLISH_GATES) { + const changed = structuredClone(base); + changed.hardGates[gate] = false; + assert.deepEqual(releaseTransitionFailures("preview-candidate", changed), []); + assert.match(releaseTransitionFailures("public-preview", changed).join("\n"), new RegExp(gate)); + } + + for (const field of ["previewLabel", "unsupportedDisclosure", "missingEvidenceVisible"] as const) { + const changed = structuredClone(base); + changed[field] = false; + assert.notDeepEqual(releaseTransitionFailures("preview-candidate", changed), []); + } + const inflated = structuredClone(base); + inflated.certificationClaim = true; + assert.match(releaseTransitionFailures("public-preview", inflated).join("\n"), /cannot claim certification/); +}); + +test("certification never inherits a public-preview waiver", () => { + const preview = previewChecks(); + const failures = releaseTransitionFailures("certified-local-core", preview).join("\n"); + assert.match(failures, /preview label/); + assert.match(failures, /unsupported status/); + assert.match(failures, /must claim its exact certified level/); + assert.match(failures, /out02/); + assert.match(failures, /out03/); + assert.match(failures, /support/); + + const certified = structuredClone(preview); + certified.previewLabel = false; + certified.unsupportedDisclosure = false; + certified.missingEvidenceVisible = false; + certified.certificationClaim = true; + certified.out02 = "pass"; + certified.out03 = "pass"; + certified.support = "pass"; + assert.deepEqual(releaseTransitionFailures("certified-local-core", certified), []); +}); + +test("release tag must match the coordinated package version", () => { + assert.deepEqual(verifyTagVersion({ version: "0.14.4" }, "v0.14.4"), []); + assert.match(verifyTagVersion({ version: "0.14.4" }, "v0.14.5").join("\n"), /does not match/); + assert.match(verifyTagVersion({}, "v0.14.4").join("\n"), /must contain a version/); +}); + +test("pack coordinate binds filename, hashes, package name, and version", () => { + assert.deepEqual( + packCoordinate( + [{ filename: "pkg-1.2.3.tgz", integrity: "sha512-abc", shasum: "def" }], + { name: "pkg", version: "1.2.3" }, + ), + { + filename: "pkg-1.2.3.tgz", + integrity: "sha512-abc", + shasum: "def", + packageSpec: "pkg@1.2.3", + }, + ); + assert.deepEqual( + packCoordinate( + { pkg: { filename: "pkg-1.2.3.tgz", integrity: "sha512-abc", shasum: "def" } }, + { name: "pkg", version: "1.2.3" }, + ), + { + filename: "pkg-1.2.3.tgz", + integrity: "sha512-abc", + shasum: "def", + packageSpec: "pkg@1.2.3", + }, + ); +}); + +test("candidate provenance binds exact tarball bytes, commit, workflow, and run", () => { + const value = candidateProvenance( + [{ filename: "pkg-1.2.3.tgz", integrity: "sha512-abc", shasum: "def" }], + { name: "pkg", version: "1.2.3" }, + "a".repeat(64), + { + repository: "owner/repo", + commit: "b".repeat(40), + workflowRef: "owner/repo/.github/workflows/ci.yml@refs/pull/1/merge", + runId: "123", + runAttempt: "2", + serverUrl: "https://github.com", + }, + ); + assert.deepEqual(value["subject"], [{ name: "pkg-1.2.3.tgz", digest: { sha256: "a".repeat(64) } }]); + assert.equal(value["predicateType"], "https://slsa.dev/provenance/v1"); + assert.match(JSON.stringify(value), /owner\/repo\/actions\/runs\/123\/attempts\/2/); + assert.throws( + () => candidateProvenance( + [{ filename: "pkg.tgz", integrity: "sha512-abc", shasum: "def" }], + { name: "pkg", version: "1.2.3" }, + "bad", + { repository: "", commit: "", workflowRef: "", runId: "", runAttempt: "", serverUrl: "" }, + ), + /SHA-256/, + ); +}); + +test("registry verification requires matching bytes, signature, and provenance", () => { + const pack = { integrity: "sha512-abc", shasum: "def" }; + assert.deepEqual( + verifyPublishedDistribution(pack, { + ...pack, + attestations: { provenance: { predicateType: "https://slsa.dev/provenance/v1" } }, + signatures: [{ keyid: "key", sig: "signature" }], + }), + [], + ); + assert.deepEqual( + verifyPublishedDistribution(pack, [{ + ...pack, + attestations: { provenance: { predicateType: "https://slsa.dev/provenance/v1" } }, + signatures: [{ keyid: "key", sig: "signature" }], + }]), + [], + ); + assert.deepEqual( + verifyPublishedDistribution(pack, { integrity: "wrong", shasum: "def", signatures: [] }), + [ + "registry integrity does not match the packed bytes", + "registry provenance attestation is missing", + "registry package signature is missing", + ], + ); +}); diff --git a/test/renderer-security.test.ts b/test/renderer-security.test.ts new file mode 100644 index 0000000..ac628e3 --- /dev/null +++ b/test/renderer-security.test.ts @@ -0,0 +1,141 @@ +import { readdir, readFile } from "node:fs/promises"; +import { test } from "node:test"; +import assert from "node:assert/strict"; +import * as vega from "vega"; +import { expressionInterpreter } from "vega-interpreter"; +import { buildTooltipMarkup, TooltipMarkupStyleCreator } from "../node_modules/echarts/lib/component/tooltip/tooltipMarkup.js"; +import { renderArtifact } from "../src/render.ts"; + +const ECHARTS_TOOLTIP_PAYLOAD = ''; +const VEGA_GLOBAL_GADGET = "({toString:event.view.VEGA_DEBUG.vega.CanvasHandler.prototype.on,eventName:event.view.console.log,_handlers:{undefined:'alert(origin)'},_handlerIndex:event.view.eval})+1"; +const VEGA_SETDATA_PAYLOAD = "setdata('table',[['cookies: '+event.dataflow._el.ownerDocument.cookie]])+warn('XSS',modify('table',2,3,null,event.dataflow._el.ownerDocument.defaultView.alert,{'x':'y'}))"; + +async function installedVersion(name: string): Promise { + const value = JSON.parse(await readFile(new URL(`../node_modules/${name}/package.json`, import.meta.url), "utf8")) as unknown; + if (typeof value !== "object" || value === null || !("version" in value) || typeof value.version !== "string") { + throw new Error(`${name} has no installed version`); + } + return value.version; +} + +async function markdownFiles(directory: URL): Promise { + const files: URL[] = []; + for (const entry of await readdir(directory, { withFileTypes: true })) { + const child = new URL(entry.name + (entry.isDirectory() ? "/" : ""), directory); + if (entry.isDirectory()) files.push(...await markdownFiles(child)); + else if (entry.name.endsWith(".md")) files.push(child); + } + return files; +} + +test("renderer dependency family is on the approved patched versions", async () => { + assert.deepEqual( + await Promise.all(["echarts", "vega", "vega-lite", "vega-embed", "vega-functions", "vega-interpreter"].map(installedVersion)), + ["6.1.0", "6.4.0", "6.4.3", "7.1.0", "6.2.0", "2.3.2"], + ); +}); + +test("every canonical example resolves under the upgraded renderer family", async () => { + const examples = await markdownFiles(new URL("../examples/", import.meta.url)); + assert.ok(examples.length >= 10); + let vegaExamples = 0; + let echartsExamples = 0; + for (const file of examples) { + const markdown = await readFile(file, "utf8"); + const rendered = renderArtifact(markdown); + const expectedInlineError = file.pathname.endsWith("/examples/incident-report.md"); + assert.equal( + rendered.html.includes('"error":"'), + expectedInlineError, + `${file.pathname} had an unexpected chart-error disposition`, + ); + if (markdown.includes("```vega-lite") || markdown.includes("```vega\n")) { + vegaExamples++; + assert.ok(rendered.html.includes("runtime:vega"), `${file.pathname} omitted Vega`); + } + if (markdown.includes("```echarts")) { + echartsExamples++; + assert.ok(rendered.html.includes("runtime:echarts"), `${file.pathname} omitted ECharts`); + } + } + assert.ok(vegaExamples >= 4); + assert.ok(echartsExamples >= 3); +}); + +test("ECharts built-in HTML tooltip markup encodes attacker-controlled names and values", () => { + const markup = buildTooltipMarkup( + { + type: "section", + header: ECHARTS_TOOLTIP_PAYLOAD, + blocks: [{ type: "nameValue", name: ECHARTS_TOOLTIP_PAYLOAD, value: ECHARTS_TOOLTIP_PAYLOAD, noMarker: true }], + }, + new TooltipMarkupStyleCreator(), + "html", + undefined, + false, + {}, + ); + assert.equal(typeof markup, "string"); + assert.ok(!markup.includes(" { + const runtime = vega.parse( + { + signals: [{ name: "answer", value: 1, update: "answer + 1" }], + marks: [], + }, + null, + { ast: true }, + ); + const original = Object.getOwnPropertyDescriptor(globalThis, "Function"); + let constructorCalls = 0; + Object.defineProperty(globalThis, "Function", { + configurable: true, + writable: true, + value: () => { + constructorCalls++; + throw new Error("Function constructor is forbidden"); + }, + }); + try { + const view = new vega.View(runtime, { expr: expressionInterpreter, renderer: "none" }); + await view.runAsync(); + assert.equal(view.signal("answer"), 2); + view.finalize(); + } finally { + if (original) Object.defineProperty(globalThis, "Function", original); + } + assert.equal(constructorCalls, 0); +}); + +test("advisory payloads stay data in the page and the app exposes no Vega View global", () => { + const markdown = [ + "```vega", + JSON.stringify({ + data: [{ name: "table", values: [{ category: "A", amount: 28 }] }], + signals: [ + { name: "gadget", value: null, on: [{ events: { type: "mousemove", source: "window" }, update: VEGA_GLOBAL_GADGET }] }, + { name: "setdata", value: null, on: [{ events: { type: "timer", throttle: 2000 }, update: VEGA_SETDATA_PAYLOAD }] }, + ], + }), + "```", + "```echarts", + JSON.stringify({ + tooltip: {}, + xAxis: {}, + yAxis: {}, + series: [{ type: "lines", data: [{ name: ECHARTS_TOOLTIP_PAYLOAD, coords: [[0, 0], [1, 1]] }] }], + }), + "```", + ].join("\n"); + const { html } = renderArtifact(markdown); + assert.ok(html.includes("ast: true")); + assert.ok(html.includes("Promise.resolve(window.vegaEmbed")); + assert.ok(!html.includes("window.VEGA_DEBUG=")); + assert.ok(!html.includes("__ARTIFACT_VEGA_VIEW__")); + assert.ok(!html.includes("