From 06033183fbf55d7a7ce79f9cb879975d55b02279 Mon Sep 17 00:00:00 2001 From: bitgorust Date: Sun, 16 Aug 2026 02:47:31 +0200 Subject: [PATCH 01/18] docs: propose phase zero contract packets --- .../contract-link-integrity/change.json | 25 ++ .../changes/contract-link-integrity/delta.md | 29 ++ .../contract-link-integrity/evidence.md | 9 + .../contract-link-integrity/proposal.md | 45 ++ .../changes/contract-link-integrity/tasks.md | 9 + .../journey-corpus-and-baseline/change.json | 28 ++ .../journey-corpus-and-baseline/delta.md | 106 +++++ .../journey-corpus-and-baseline/evidence.md | 25 ++ .../journey-corpus-and-baseline/proposal.md | 48 +++ .../journey-corpus-and-baseline/tasks.md | 10 + .../change.json | 39 ++ .../platform-security-privacy-policy/delta.md | 384 ++++++++++++++++++ .../design.md | 79 ++++ .../evidence.md | 78 ++++ .../proposal.md | 55 +++ .../platform-security-privacy-policy/tasks.md | 12 + 16 files changed, 981 insertions(+) create mode 100644 specs/changes/contract-link-integrity/change.json create mode 100644 specs/changes/contract-link-integrity/delta.md create mode 100644 specs/changes/contract-link-integrity/evidence.md create mode 100644 specs/changes/contract-link-integrity/proposal.md create mode 100644 specs/changes/contract-link-integrity/tasks.md create mode 100644 specs/changes/journey-corpus-and-baseline/change.json create mode 100644 specs/changes/journey-corpus-and-baseline/delta.md create mode 100644 specs/changes/journey-corpus-and-baseline/evidence.md create mode 100644 specs/changes/journey-corpus-and-baseline/proposal.md create mode 100644 specs/changes/journey-corpus-and-baseline/tasks.md create mode 100644 specs/changes/platform-security-privacy-policy/change.json create mode 100644 specs/changes/platform-security-privacy-policy/delta.md create mode 100644 specs/changes/platform-security-privacy-policy/design.md create mode 100644 specs/changes/platform-security-privacy-policy/evidence.md create mode 100644 specs/changes/platform-security-privacy-policy/proposal.md create mode 100644 specs/changes/platform-security-privacy-policy/tasks.md diff --git a/specs/changes/contract-link-integrity/change.json b/specs/changes/contract-link-integrity/change.json new file mode 100644 index 0000000..1657954 --- /dev/null +++ b/specs/changes/contract-link-integrity/change.json @@ -0,0 +1,25 @@ +{ + "schemaVersion": 1, + "id": "contract-link-integrity", + "title": "Validate documentation links and anchors", + "lane": "standard", + "status": "draft", + "affectedRequirements": [ + "QUAL-08" + ], + "currentSpecs": [ + "specs/current/contract-integrity.spec.md" + ], + "currentSpecsUpdated": false, + "approval": { + "by": "", + "at": "" + }, + "withdrawal": { + "by": "", + "at": "", + "reason": "" + }, + "createdAt": "2026-08-16", + "archivedAt": null +} diff --git a/specs/changes/contract-link-integrity/delta.md b/specs/changes/contract-link-integrity/delta.md new file mode 100644 index 0000000..eb4d33b --- /dev/null +++ b/specs/changes/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/changes/contract-link-integrity/evidence.md b/specs/changes/contract-link-integrity/evidence.md new file mode 100644 index 0000000..8b02e56 --- /dev/null +++ b/specs/changes/contract-link-integrity/evidence.md @@ -0,0 +1,9 @@ +# Evidence: Validate documentation links and anchors + +## Requirement: QUAL-08 + +- Validation: pending maintainer approval and review of diagnostic usefulness. +- Verification: pending implementation tests and a dated official-source run. +- Result: pending; this draft packet does not establish link integrity. +- Planned evidence: parser/local/fake-network test file and a retained report under + `docs/evidence/contract/`, to be linked only after they exist and execute. diff --git a/specs/changes/contract-link-integrity/proposal.md b/specs/changes/contract-link-integrity/proposal.md new file mode 100644 index 0000000..89f9a76 --- /dev/null +++ b/specs/changes/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/changes/contract-link-integrity/tasks.md b/specs/changes/contract-link-integrity/tasks.md new file mode 100644 index 0000000..cb90589 --- /dev/null +++ b/specs/changes/contract-link-integrity/tasks.md @@ -0,0 +1,9 @@ +# Tasks: Validate documentation links and anchors + +- [ ] Confirm proposal validation and human approval. +- [ ] Add table-driven parser, local-target, anchor, and fake-network tests for every scenario. +- [ ] Implement deterministic local validation and the bounded official-source command. +- [ ] Register the local invariant in repository checks and CI; retain a dated real-source run. +- [ ] Record validation and verification evidence. +- [ ] Add `specs/current/contract-integrity.spec.md` and reconcile Phase 0 status. +- [ ] Run repository validation and archive the packet. diff --git a/specs/changes/journey-corpus-and-baseline/change.json b/specs/changes/journey-corpus-and-baseline/change.json new file mode 100644 index 0000000..ae9961d --- /dev/null +++ b/specs/changes/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": "draft", + "affectedRequirements": [ + "OUT-02", + "OUT-03", + "OUT-05", + "UX-01" + ], + "currentSpecs": [ + "specs/current/journey-evidence.spec.md" + ], + "currentSpecsUpdated": false, + "approval": { + "by": "", + "at": "" + }, + "withdrawal": { + "by": "", + "at": "", + "reason": "" + }, + "createdAt": "2026-08-16", + "archivedAt": null +} diff --git a/specs/changes/journey-corpus-and-baseline/delta.md b/specs/changes/journey-corpus-and-baseline/delta.md new file mode 100644 index 0000000..4fced8a --- /dev/null +++ b/specs/changes/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/changes/journey-corpus-and-baseline/evidence.md b/specs/changes/journey-corpus-and-baseline/evidence.md new file mode 100644 index 0000000..9e018a5 --- /dev/null +++ b/specs/changes/journey-corpus-and-baseline/evidence.md @@ -0,0 +1,25 @@ +# Evidence: Establish the journey corpus and baseline study + +## Requirement: OUT-02 + +- Validation: missing; no consented first-time-user run has been conducted. +- Verification: pending approved corpus and deterministic result validation. +- Result: missing, which is not a pass. + +## Requirement: OUT-03 + +- Validation: missing; there are no results from ten consented representative primary users. +- Verification: pending approved scoring and aggregation tests. +- Result: missing, which keeps the Phase 0 gate failed. + +## Requirement: OUT-05 + +- Validation: pending maintainer review of informed consent and withdrawal language. +- Verification: pending tests that reject absent consent and unnecessary identifying fields. +- Result: pending. + +## Requirement: UX-01 + +- Validation: pending participant use of the create/revise/review/share corpus. +- Verification: pending corpus-schema and journey-completeness tests. +- Result: pending. diff --git a/specs/changes/journey-corpus-and-baseline/proposal.md b/specs/changes/journey-corpus-and-baseline/proposal.md new file mode 100644 index 0000000..eb23a88 --- /dev/null +++ b/specs/changes/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/changes/journey-corpus-and-baseline/tasks.md b/specs/changes/journey-corpus-and-baseline/tasks.md new file mode 100644 index 0000000..f6ae12a --- /dev/null +++ b/specs/changes/journey-corpus-and-baseline/tasks.md @@ -0,0 +1,10 @@ +# Tasks: Establish the journey corpus and baseline study + +- [ ] Confirm proposal validation and human approval. +- [ ] Check in the corpus, consent/withdrawal protocol, inclusion rubric, and blank record schema. +- [ ] Add deterministic schema, scoring, threshold, exclusion, and redaction tests. +- [ ] Implement a dependency-free validator/aggregator that cannot turn missing runs into passes. +- [ ] Run OUT-02 and OUT-03 with consented representative participants and retain real results. +- [ ] Record validation and verification evidence. +- [ ] Add `specs/current/journey-evidence.spec.md` and reconcile Phase 0 status. +- [ ] Run repository validation and archive the packet. diff --git a/specs/changes/platform-security-privacy-policy/change.json b/specs/changes/platform-security-privacy-policy/change.json new file mode 100644 index 0000000..ec1e3f5 --- /dev/null +++ b/specs/changes/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": "draft", + "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": false, + "approval": { + "by": "", + "at": "" + }, + "withdrawal": { + "by": "", + "at": "", + "reason": "" + }, + "createdAt": "2026-08-16", + "archivedAt": null +} diff --git a/specs/changes/platform-security-privacy-policy/delta.md b/specs/changes/platform-security-privacy-policy/delta.md new file mode 100644 index 0000000..eedef6e --- /dev/null +++ b/specs/changes/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/changes/platform-security-privacy-policy/design.md b/specs/changes/platform-security-privacy-policy/design.md new file mode 100644 index 0000000..66568ae --- /dev/null +++ b/specs/changes/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/changes/platform-security-privacy-policy/evidence.md b/specs/changes/platform-security-privacy-policy/evidence.md new file mode 100644 index 0000000..666abd1 --- /dev/null +++ b/specs/changes/platform-security-privacy-policy/evidence.md @@ -0,0 +1,78 @@ +# Evidence: Publish platform, security, privacy, and release policy + +This draft records known evidence state; links are added only after approved artifacts exist. + +## Requirement: SEC-01 +- Validation: pending accountable security review. +- Verification: no versioned threat model exists. +- Result: missing. + +## Requirement: SEC-10 +- Validation: pending response-policy review. +- Verification: GitHub private vulnerability reporting returned `enabled: false` on 2026-08-16. +- Result: fail; there is no verified private reporting path. + +## Requirement: PRIV-01 +- Validation: pending privacy review. +- Verification: no versioned mode-specific data inventory exists. +- Result: missing. + +## Requirement: PRIV-02 +- Validation: pending policy review. +- Verification: current code audit and tests must prove no default telemetry after approval. +- Result: pending. + +## Requirement: PRIV-03 +- Validation: pending policy review. +- Verification: existing secret/publication tests are incomplete evidence until mapped to every output boundary. +- Result: partial. + +## Requirement: PRIV-04 +- Validation: pending operator disclosure review. +- Verification: target-specific operator/region/recipient disclosure is incomplete. +- Result: missing. + +## Requirement: PRIV-05 +- Validation: pending deletion-consequence review. +- Verification: mode-specific list/export/correct/delete and backup-expiry evidence is absent. +- Result: missing. + +## Requirement: PRIV-06 +- Validation: pending evidence-retention review. +- Verification: no canonical minimization and retention policy exists. +- Result: missing. + +## Requirement: PRIV-07 +- Validation: pending public abuse/IP review. +- Verification: no canonical reporting/takedown policy exists. +- Result: missing. + +## Requirement: COMPAT-01 +- Validation: pending support-scope approval. +- Verification: OpenCode 1.18.18 has dated Ubuntu-local evidence; required macOS, Windows, browser, mobile, and second-host evidence is unavailable. +- Result: fail for the target matrix. + +## Requirement: DIST-03 +- Validation: pending release-policy review. +- Verification: SemVer and Conventional Commit practice exist, but canonical claim/limits/evidence release policy is incomplete. +- Result: partial. + +## Requirement: DIST-04 +- Validation: pending D-06 approval. +- Verification: publish workflow requests npm provenance; no retained SBOM, vulnerability/license output, or registry-side attestation verification exists. +- Result: partial and insufficient. + +## Requirement: DIST-05 +- Validation: pending dependency/license policy review. +- Verification: lockfile exists; complete license/network/CSP/removal disposition is absent. +- Result: partial. + +## Requirement: DIST-06 +- Validation: pending support-window approval. +- Verification: supported-version, deprecation, vulnerability-response, and end-of-life policy is absent. +- Result: missing. + +## Requirement: DIST-07 +- Validation: pending attribution policy review. +- Verification: repository license exists; a complete documentation/assets/font/reference inventory is absent. +- Result: partial. diff --git a/specs/changes/platform-security-privacy-policy/proposal.md b/specs/changes/platform-security-privacy-policy/proposal.md new file mode 100644 index 0000000..fe260b2 --- /dev/null +++ b/specs/changes/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/changes/platform-security-privacy-policy/tasks.md b/specs/changes/platform-security-privacy-policy/tasks.md new file mode 100644 index 0000000..f08f085 --- /dev/null +++ b/specs/changes/platform-security-privacy-policy/tasks.md @@ -0,0 +1,12 @@ +# Tasks: Publish platform, security, privacy, and release policy + +- [ ] Confirm proposal validation and human approval. +- [ ] Publish canonical support/D-01/D-06, security/threat, data-governance, and public vulnerability policies. +- [ ] Add deterministic policy completeness, matrix-evidence, inventory, and claim-consistency tests. +- [ ] Generate and inspect CycloneDX, vulnerability, signature, license, and packed-byte outputs. +- [ ] Run every claimed host/browser cell and retain exact real results; leave unavailable cells unverified. +- [ ] Obtain explicit authority before enabling GitHub private vulnerability reporting or changing npm trusted publishing. +- [ ] Verify external reporting/provenance state rather than inferring it from configuration. +- [ ] Record validation and verification evidence. +- [ ] Add `specs/current/governance-policy.spec.md` and reconcile README, roadmap, traceability, hosting, comparison, and release template. +- [ ] Run repository validation and archive the packet. From 116d83f61e0b5964db3c254aedb96e06bc41ef00 Mon Sep 17 00:00:00 2001 From: bitgorust Date: Sun, 16 Aug 2026 10:18:00 +0200 Subject: [PATCH 02/18] docs: record phase zero approvals --- specs/changes/contract-link-integrity/change.json | 6 +++--- specs/changes/journey-corpus-and-baseline/change.json | 6 +++--- specs/changes/platform-security-privacy-policy/change.json | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/specs/changes/contract-link-integrity/change.json b/specs/changes/contract-link-integrity/change.json index 1657954..906c44d 100644 --- a/specs/changes/contract-link-integrity/change.json +++ b/specs/changes/contract-link-integrity/change.json @@ -3,7 +3,7 @@ "id": "contract-link-integrity", "title": "Validate documentation links and anchors", "lane": "standard", - "status": "draft", + "status": "approved", "affectedRequirements": [ "QUAL-08" ], @@ -12,8 +12,8 @@ ], "currentSpecsUpdated": false, "approval": { - "by": "", - "at": "" + "by": "bitgorust", + "at": "2026-08-16T08:17:19Z" }, "withdrawal": { "by": "", diff --git a/specs/changes/journey-corpus-and-baseline/change.json b/specs/changes/journey-corpus-and-baseline/change.json index ae9961d..340e06e 100644 --- a/specs/changes/journey-corpus-and-baseline/change.json +++ b/specs/changes/journey-corpus-and-baseline/change.json @@ -3,7 +3,7 @@ "id": "journey-corpus-and-baseline", "title": "Establish the journey corpus and baseline study", "lane": "standard", - "status": "draft", + "status": "approved", "affectedRequirements": [ "OUT-02", "OUT-03", @@ -15,8 +15,8 @@ ], "currentSpecsUpdated": false, "approval": { - "by": "", - "at": "" + "by": "bitgorust", + "at": "2026-08-16T08:17:19Z" }, "withdrawal": { "by": "", diff --git a/specs/changes/platform-security-privacy-policy/change.json b/specs/changes/platform-security-privacy-policy/change.json index ec1e3f5..86283f4 100644 --- a/specs/changes/platform-security-privacy-policy/change.json +++ b/specs/changes/platform-security-privacy-policy/change.json @@ -3,7 +3,7 @@ "id": "platform-security-privacy-policy", "title": "Publish platform, security, privacy, and release policy", "lane": "high-risk", - "status": "draft", + "status": "approved", "affectedRequirements": [ "SEC-01", "SEC-10", @@ -26,8 +26,8 @@ ], "currentSpecsUpdated": false, "approval": { - "by": "", - "at": "" + "by": "bitgorust", + "at": "2026-08-16T08:17:19Z" }, "withdrawal": { "by": "", From f2342646ef107082c6ab1b54cb05f5cf1fd33f09 Mon Sep 17 00:00:00 2001 From: bitgorust Date: Sun, 16 Aug 2026 10:26:53 +0200 Subject: [PATCH 03/18] feat: validate contract documentation links --- docs/engineering-principles.md | 3 + .../contract/official-links-2026-08-16.md | 45 +++ docs/requirements-traceability.md | 1 + docs/roadmap.md | 2 +- package.json | 1 + scripts/check-links.ts | 37 +++ scripts/check-repo.ts | 12 + scripts/checks.ts | 2 + scripts/documentation-links.ts | 310 ++++++++++++++++++ .../change.json | 6 +- .../delta.md | 0 .../evidence.md | 12 + .../proposal.md | 0 .../tasks.md | 9 + .../contract-link-integrity/evidence.md | 9 - .../changes/contract-link-integrity/tasks.md | 9 - specs/current/contract-integrity.spec.md | 21 ++ test/documentation-links.test.ts | 108 ++++++ 18 files changed, 565 insertions(+), 22 deletions(-) create mode 100644 docs/evidence/contract/official-links-2026-08-16.md create mode 100644 scripts/check-links.ts create mode 100644 scripts/documentation-links.ts rename specs/{changes/contract-link-integrity => archive/2026-08-16-contract-link-integrity}/change.json (83%) rename specs/{changes/contract-link-integrity => archive/2026-08-16-contract-link-integrity}/delta.md (100%) create mode 100644 specs/archive/2026-08-16-contract-link-integrity/evidence.md rename specs/{changes/contract-link-integrity => archive/2026-08-16-contract-link-integrity}/proposal.md (100%) create mode 100644 specs/archive/2026-08-16-contract-link-integrity/tasks.md delete mode 100644 specs/changes/contract-link-integrity/evidence.md delete mode 100644 specs/changes/contract-link-integrity/tasks.md create mode 100644 specs/current/contract-integrity.spec.md create mode 100644 test/documentation-links.test.ts diff --git a/docs/engineering-principles.md b/docs/engineering-principles.md index 8e51949..47f9556 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 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/requirements-traceability.md b/docs/requirements-traceability.md index 2c2a038..4b13781 100644 --- a/docs/requirements-traceability.md +++ b/docs/requirements-traceability.md @@ -66,6 +66,7 @@ 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 | | 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` | diff --git a/docs/roadmap.md b/docs/roadmap.md index 1c8a74a..629ee10 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -85,7 +85,7 @@ 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 diff --git a/package.json b/package.json index 581a681..69e3016 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "build": "tsc -p tsconfig.json", "test": "node --test test/*.test.ts", "check": "node scripts/check-repo.ts", + "check:links": "node scripts/check-links.ts", "spec": "node scripts/spec-workflow.ts", "prepare": "git config core.hooksPath .githooks || true", "prepublishOnly": "npm run build" 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..fdf3fbb 100644 --- a/scripts/check-repo.ts +++ b/scripts/check-repo.ts @@ -3,6 +3,7 @@ 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"; const root = join(import.meta.dirname, ".."); @@ -74,6 +75,17 @@ 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 "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..781e6d9 100644 --- a/scripts/checks.ts +++ b/scripts/checks.ts @@ -6,6 +6,7 @@ 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: "spec-workflow" } | { id: string; kind: "package-field"; field: "version-semver" | "metadata" | "files-skills" }; @@ -20,6 +21,7 @@ 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: "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/specs/changes/contract-link-integrity/change.json b/specs/archive/2026-08-16-contract-link-integrity/change.json similarity index 83% rename from specs/changes/contract-link-integrity/change.json rename to specs/archive/2026-08-16-contract-link-integrity/change.json index 906c44d..f49baf7 100644 --- a/specs/changes/contract-link-integrity/change.json +++ b/specs/archive/2026-08-16-contract-link-integrity/change.json @@ -3,14 +3,14 @@ "id": "contract-link-integrity", "title": "Validate documentation links and anchors", "lane": "standard", - "status": "approved", + "status": "archived", "affectedRequirements": [ "QUAL-08" ], "currentSpecs": [ "specs/current/contract-integrity.spec.md" ], - "currentSpecsUpdated": false, + "currentSpecsUpdated": true, "approval": { "by": "bitgorust", "at": "2026-08-16T08:17:19Z" @@ -21,5 +21,5 @@ "reason": "" }, "createdAt": "2026-08-16", - "archivedAt": null + "archivedAt": "2026-08-16" } diff --git a/specs/changes/contract-link-integrity/delta.md b/specs/archive/2026-08-16-contract-link-integrity/delta.md similarity index 100% rename from specs/changes/contract-link-integrity/delta.md rename to specs/archive/2026-08-16-contract-link-integrity/delta.md 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/changes/contract-link-integrity/proposal.md b/specs/archive/2026-08-16-contract-link-integrity/proposal.md similarity index 100% rename from specs/changes/contract-link-integrity/proposal.md rename to specs/archive/2026-08-16-contract-link-integrity/proposal.md 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/changes/contract-link-integrity/evidence.md b/specs/changes/contract-link-integrity/evidence.md deleted file mode 100644 index 8b02e56..0000000 --- a/specs/changes/contract-link-integrity/evidence.md +++ /dev/null @@ -1,9 +0,0 @@ -# Evidence: Validate documentation links and anchors - -## Requirement: QUAL-08 - -- Validation: pending maintainer approval and review of diagnostic usefulness. -- Verification: pending implementation tests and a dated official-source run. -- Result: pending; this draft packet does not establish link integrity. -- Planned evidence: parser/local/fake-network test file and a retained report under - `docs/evidence/contract/`, to be linked only after they exist and execute. diff --git a/specs/changes/contract-link-integrity/tasks.md b/specs/changes/contract-link-integrity/tasks.md deleted file mode 100644 index cb90589..0000000 --- a/specs/changes/contract-link-integrity/tasks.md +++ /dev/null @@ -1,9 +0,0 @@ -# Tasks: Validate documentation links and anchors - -- [ ] Confirm proposal validation and human approval. -- [ ] Add table-driven parser, local-target, anchor, and fake-network tests for every scenario. -- [ ] Implement deterministic local validation and the bounded official-source command. -- [ ] Register the local invariant in repository checks and CI; retain a dated real-source run. -- [ ] Record validation and verification evidence. -- [ ] Add `specs/current/contract-integrity.spec.md` and reconcile Phase 0 status. -- [ ] Run repository validation 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/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 }); + } +}); From 0201e316c63c92d865752614a38b07bf4776e819 Mon Sep 17 00:00:00 2001 From: bitgorust Date: Sun, 16 Aug 2026 10:36:50 +0200 Subject: [PATCH 04/18] feat: add truthful journey study harness --- .gitignore | 1 + .../journeys/phase-0-baseline-status.md | 14 + docs/journeys/README.md | 104 +++++ docs/journeys/corpus.json | 108 +++++ docs/journeys/fixtures/incident.md | 16 + docs/journeys/fixtures/migration.md | 19 + docs/journeys/fixtures/release.md | 18 + docs/journeys/fixtures/review.md | 14 + docs/journeys/records.template.json | 12 + docs/journeys/study.schema.json | 133 ++++++ docs/requirements-traceability.md | 3 +- package.json | 1 + scripts/journey-study-lib.ts | 426 ++++++++++++++++++ scripts/journey-study.ts | 26 ++ .../journey-corpus-and-baseline/change.json | 4 +- .../journey-corpus-and-baseline/evidence.md | 30 +- .../journey-corpus-and-baseline/tasks.md | 12 +- specs/current/journey-evidence.spec.md | 35 ++ test/journey-study.test.ts | 163 +++++++ 19 files changed, 1122 insertions(+), 17 deletions(-) create mode 100644 docs/evidence/journeys/phase-0-baseline-status.md create mode 100644 docs/journeys/README.md create mode 100644 docs/journeys/corpus.json create mode 100644 docs/journeys/fixtures/incident.md create mode 100644 docs/journeys/fixtures/migration.md create mode 100644 docs/journeys/fixtures/release.md create mode 100644 docs/journeys/fixtures/review.md create mode 100644 docs/journeys/records.template.json create mode 100644 docs/journeys/study.schema.json create mode 100644 scripts/journey-study-lib.ts create mode 100644 scripts/journey-study.ts create mode 100644 specs/current/journey-evidence.spec.md create mode 100644 test/journey-study.test.ts 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/docs/evidence/journeys/phase-0-baseline-status.md b/docs/evidence/journeys/phase-0-baseline-status.md new file mode 100644 index 0000000..078ef9a --- /dev/null +++ b/docs/evidence/journeys/phase-0-baseline-status.md @@ -0,0 +1,14 @@ +# 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`: missing; no eligible first-time-user clean-machine result +- `OUT-03`: missing; 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. +Phase 0 remains incomplete until the approved protocol is executed with real consented people. 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/requirements-traceability.md b/docs/requirements-traceability.md index 4b13781..4faedca 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 @@ -67,6 +67,7 @@ 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` | diff --git a/package.json b/package.json index 69e3016..377a2c8 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "test": "node --test test/*.test.ts", "check": "node scripts/check-repo.ts", "check:links": "node scripts/check-links.ts", + "study": "node scripts/journey-study.ts", "spec": "node scripts/spec-workflow.ts", "prepare": "git config core.hooksPath .githooks || true", "prepublishOnly": "npm run build" 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/specs/changes/journey-corpus-and-baseline/change.json b/specs/changes/journey-corpus-and-baseline/change.json index 340e06e..651b328 100644 --- a/specs/changes/journey-corpus-and-baseline/change.json +++ b/specs/changes/journey-corpus-and-baseline/change.json @@ -3,7 +3,7 @@ "id": "journey-corpus-and-baseline", "title": "Establish the journey corpus and baseline study", "lane": "standard", - "status": "approved", + "status": "implementing", "affectedRequirements": [ "OUT-02", "OUT-03", @@ -13,7 +13,7 @@ "currentSpecs": [ "specs/current/journey-evidence.spec.md" ], - "currentSpecsUpdated": false, + "currentSpecsUpdated": true, "approval": { "by": "bitgorust", "at": "2026-08-16T08:17:19Z" diff --git a/specs/changes/journey-corpus-and-baseline/evidence.md b/specs/changes/journey-corpus-and-baseline/evidence.md index 9e018a5..2aa0028 100644 --- a/specs/changes/journey-corpus-and-baseline/evidence.md +++ b/specs/changes/journey-corpus-and-baseline/evidence.md @@ -3,23 +3,37 @@ ## Requirement: OUT-02 - Validation: missing; no consented first-time-user run has been conducted. -- Verification: pending approved corpus and deterministic result validation. +- 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: missing, which is not a pass. +- 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: pending approved scoring and aggregation tests. +- Verification: focused tests exercise the exact ten-participant and 90% boundary, exclusion + rules, fixture distribution, and the empty-study result. - Result: missing, which keeps the Phase 0 gate failed. +- Evidence: [@test](test/journey-study.test.ts), + [@manual](docs/evidence/journeys/phase-0-baseline-status.md) ## Requirement: OUT-05 -- Validation: pending maintainer review of informed consent and withdrawal language. -- Verification: pending tests that reject absent consent and unnecessary identifying fields. -- Result: pending. +- 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: pending participant use of the create/revise/review/share corpus. -- Verification: pending corpus-schema and journey-completeness tests. -- Result: pending. +- 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/changes/journey-corpus-and-baseline/tasks.md b/specs/changes/journey-corpus-and-baseline/tasks.md index f6ae12a..647c56c 100644 --- a/specs/changes/journey-corpus-and-baseline/tasks.md +++ b/specs/changes/journey-corpus-and-baseline/tasks.md @@ -1,10 +1,10 @@ # Tasks: Establish the journey corpus and baseline study -- [ ] Confirm proposal validation and human approval. -- [ ] Check in the corpus, consent/withdrawal protocol, inclusion rubric, and blank record schema. -- [ ] Add deterministic schema, scoring, threshold, exclusion, and redaction tests. -- [ ] Implement a dependency-free validator/aggregator that cannot turn missing runs into passes. +- [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. - [ ] Run OUT-02 and OUT-03 with consented representative participants and retain real results. -- [ ] Record validation and verification evidence. -- [ ] Add `specs/current/journey-evidence.spec.md` and reconcile Phase 0 status. +- [x] Record validation and verification evidence. +- [x] Add `specs/current/journey-evidence.spec.md` and reconcile Phase 0 status. - [ ] Run repository validation and archive the packet. diff --git a/specs/current/journey-evidence.spec.md b/specs/current/journey-evidence.spec.md new file mode 100644 index 0000000..3b99eac --- /dev/null +++ b/specs/current/journey-evidence.spec.md @@ -0,0 +1,35 @@ +# 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 missing and the Phase 0 gate remains + incomplete until the approved real study is run. 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/); +}); From ee2dd1eb68834b7a868ca15b144722a6044df7ec Mon Sep 17 00:00:00 2001 From: bitgorust Date: Sun, 16 Aug 2026 12:24:26 +0200 Subject: [PATCH 05/18] docs: publish truthful governance policies --- README.md | 28 +- SECURITY.md | 21 ++ docs/claude-code-comparison.md | 3 + docs/data-governance.md | 76 +++++ docs/engineering-principles.md | 4 +- .../local-environment-2026-08-16.md | 17 ++ .../governance/provider-status-2026-08-16.md | 12 + .../governance/supply-chain-2026-08-16.md | 86 ++++++ docs/governance-policy.json | 239 +++++++++++++++ docs/hosted-cloudflare.md | 24 +- docs/release-evidence-template.md | 10 +- docs/requirements-traceability.md | 5 +- docs/roadmap.md | 8 +- docs/security.md | 43 +++ docs/support-policy.md | 84 +++++ docs/threat-model.md | 115 +++++++ package-lock.json | 2 +- package.json | 3 +- scripts/check-repo.ts | 6 + scripts/checks.ts | 2 + scripts/governance-policy.ts | 287 ++++++++++++++++++ .../change.json | 4 +- .../evidence.md | 137 ++++++--- .../platform-security-privacy-policy/tasks.md | 14 +- specs/current/governance-policy.spec.md | 34 +++ test/governance-policy.test.ts | 49 +++ 26 files changed, 1236 insertions(+), 77 deletions(-) create mode 100644 SECURITY.md create mode 100644 docs/data-governance.md create mode 100644 docs/evidence/governance/local-environment-2026-08-16.md create mode 100644 docs/evidence/governance/provider-status-2026-08-16.md create mode 100644 docs/evidence/governance/supply-chain-2026-08-16.md create mode 100644 docs/governance-policy.json create mode 100644 docs/security.md create mode 100644 docs/support-policy.md create mode 100644 docs/threat-model.md create mode 100644 scripts/governance-policy.ts create mode 100644 specs/current/governance-policy.spec.md create mode 100644 test/governance-policy.test.ts diff --git a/README.md b/README.md index a24f50b..f3555be 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,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 +48,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 +57,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 +166,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 +178,22 @@ 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 policy is explicit about incomplete 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 unavailable; +- [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 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..7b383a5 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security policy + +## Reporting status + +Private vulnerability reporting is currently **unavailable**. GitHub private vulnerability +reporting was checked on 2026-08-16 and was disabled. Do not place exploit details, secrets, +private artifact content, or personal data in a public issue. +GitHub documents that its private report form exists only after repository owners +[enable private vulnerability reporting](https://docs.github.com/en/code-security/how-tos/report-and-fix-vulnerabilities/configure-vulnerability-reporting/configure-for-a-repository). + +Non-sensitive hardening bugs may be reported through the public issue tracker with a minimal +reproduction that contains no confidential data. Until an authorized maintainer enables and +verifies private reporting, this repository does not claim a safe intake path for sensitive +vulnerability reports and does not meet its production-readiness security gate. + +## 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..d27723f 100644 --- a/docs/claude-code-comparison.md +++ b/docs/claude-code-comparison.md @@ -16,6 +16,9 @@ The official viewer screenshot is retained at `docs/references/claude-artifact-v 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/data-governance.md b/docs/data-governance.md new file mode 100644 index 0000000..eab0181 --- /dev/null +++ b/docs/data-governance.md @@ -0,0 +1,76 @@ +# 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). + +## Public abuse, takedown, and intellectual property + +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 47f9556..86a0519 100644 --- a/docs/engineering-principles.md +++ b/docs/engineering-principles.md @@ -208,7 +208,9 @@ 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] - 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/governance/local-environment-2026-08-16.md b/docs/evidence/governance/local-environment-2026-08-16.md new file mode 100644 index 0000000..239850e --- /dev/null +++ b/docs/evidence/governance/local-environment-2026-08-16.md @@ -0,0 +1,17 @@ +# 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. 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..c9d527d --- /dev/null +++ b/docs/evidence/governance/provider-status-2026-08-16.md @@ -0,0 +1,12 @@ +# Provider prerequisite status — 2026-08-16 + +Repository: `bitgorust/opencode-artifacts` + +| Prerequisite | Observed result | Consequence | +|---|---|---| +| GitHub private vulnerability reporting | Repository API returned `private_vulnerability_reporting.enabled: false` | Failed; `SECURITY.md` cannot direct sensitive reports to a verified private 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 / future provenance | Provider configuration is not publicly verified and no future candidate is published | Unverified; prior provenance and workflow configuration cannot pass a future release | + +No provider setting was changed during this observation. Enabling either setting requires an +explicitly authorized external-state checkpoint and follow-up verification. 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..8b08163 --- /dev/null +++ b/docs/evidence/governance/supply-chain-2026-08-16.md @@ -0,0 +1,86 @@ +# 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 and provenance for this unreleased candidate remain +unverified. No registry setting or package was changed. + +## 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. Until its package metadata/redistribution +authority and the alternative-license selections are reviewed and recorded, the license gate +fails. The repository source is MIT; full documentation/example/asset/reference attribution +still requires release-level inspection. diff --git a/docs/governance-policy.json b/docs/governance-policy.json new file mode 100644 index 0000000..a016083 --- /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": "failed", + "checkedAt": "2026-08-16", + "evidence": "docs/evidence/governance/provider-status-2026-08-16.md", + "claim": "Private vulnerability reporting is disabled; sensitive report intake is unavailable." + }, + { + "id": "npm-trusted-publishing", + "status": "unverified", + "checkedAt": "2026-08-16", + "evidence": "docs/evidence/governance/provider-status-2026-08-16.md", + "claim": "Published 0.14.3 has registry provenance bound to its GitHub Actions tag/commit; trusted-publisher configuration and provenance for this unreleased candidate remain unverified." + } + ], + "supportCells": [ + { + "id": "observed-linux-opencode", + "class": "tested", + "status": "tested", + "os": "Linux x64 (distribution not recorded)", + "node": "not recorded by the host probe", + "opencode": "1.18.18", + "browser": "not exercised", + "scope": "Checkout and registry-package plugin registration only", + "testedAt": "2026-08-15", + "evidence": "docs/evidence/opencode-host-verification.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/release-evidence-template.md b/docs/release-evidence-template.md index b9599af..64f62a7 100644 --- a/docs/release-evidence-template.md +++ b/docs/release-evidence-template.md @@ -106,10 +106,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 diff --git a/docs/requirements-traceability.md b/docs/requirements-traceability.md index 4faedca..db565af 100644 --- a/docs/requirements-traceability.md +++ b/docs/requirements-traceability.md @@ -71,11 +71,12 @@ results. A release checklist resolves each class to dated files and command outp | 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 629ee10..06c02c3 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -56,11 +56,11 @@ 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 | Partial | support/release policy, lockfile and pack inspection exist; complete SBOM/license/vulnerability evidence and registry provenance verification remain open | “Shipped” means the current behavior exists; it does not waive any stronger acceptance rule in the product spec. @@ -92,7 +92,7 @@ users, support envelope, data handling, or acceptance evidence cannot be named. 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] 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. diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..37d5456 --- /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. Current status is deliberately not production-ready: GitHub private +vulnerability reporting was checked and is disabled, so there is no verified private intake +path for sensitive reports. + +## 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 private intake is operational | +|---|---|---| +| 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. They become operational only when private +intake is enabled and a release names 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..1b3a431 --- /dev/null +++ b/docs/support-policy.md @@ -0,0 +1,84 @@ +# 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**. The exact stable OpenCode +version in the dated host probe was tested for plugin registration on Linux x64, but that +probe did not record the distribution, +Node version, clean user journey, or browser scope required for support. CI separately runs +repository checks on Node 24/Ubuntu. Those observations cannot be combined into one platform +pass. The target matrix therefore remains unverified and blocks a production-readiness claim. + +| Cell | Target | Current status | Evidence or missing scope | +|---|---|---|---| +| OpenCode host observation | Linux x64, exact version in dated probe | Tested, not supported | [Host probe](evidence/opencode-host-verification.md); distribution/Node/browser/journey 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. + +## Release gate + +Every release uses [the release evidence template](release-evidence-template.md) and names one +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 the claim; “not applicable” requires a reason. + +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 +provenance for the then-current published release and binds it to its GitHub Actions workflow, +tag, and source commit. Registry trusted-publisher configuration is not +publicly verified, and that prior attestation cannot cover future candidate bytes. A release +still needs its own post-publish verification. Generated SBOM/provenance describes composition/ +origin; it does not prove safety. + +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..b051c09 --- /dev/null +++ b/docs/threat-model.md @@ -0,0 +1,115 @@ +# 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, final artifact scanning, fixed argument + arrays, state-directory exclusion, scoped Worker KV names, and staged-deploy unit tests. +- 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, secret scan precedes 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..31dd9ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "typescript": "^5.8.0" }, "engines": { - "node": ">=20" + "node": ">=24" }, "peerDependencies": { "@opencode-ai/plugin": ">=1.0.0" diff --git a/package.json b/package.json index 377a2c8..d779de8 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "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", "study": "node scripts/journey-study.ts", "spec": "node scripts/spec-workflow.ts", "prepare": "git config core.hooksPath .githooks || true", @@ -62,6 +63,6 @@ "typescript": "^5.8.0" }, "engines": { - "node": ">=20" + "node": ">=24" } } diff --git a/scripts/check-repo.ts b/scripts/check-repo.ts index fdf3fbb..ef08300 100644 --- a/scripts/check-repo.ts +++ b/scripts/check-repo.ts @@ -5,6 +5,7 @@ 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; @@ -86,6 +87,11 @@ async function evaluate(check: Check): Promise { ); 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 781e6d9..3224a46 100644 --- a/scripts/checks.ts +++ b/scripts/checks.ts @@ -7,6 +7,7 @@ export type Check = | { 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" }; @@ -22,6 +23,7 @@ export const CHECKS: Check[] = [ { 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/governance-policy.ts b/scripts/governance-policy.ts new file mode 100644 index 0000000..7e0a217 --- /dev/null +++ b/scripts/governance-policy.ts @@ -0,0 +1,287 @@ +import { readFile } from "node:fs/promises"; +import { join } 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", +]; + +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.security.includes("Private vulnerability reporting is currently **unavailable**")) { + errors.push("SECURITY.md must expose unavailable private reporting"); + } + if (!inputs.support.includes("no fully supported platform/browser cells")) { + errors.push("support policy must expose the empty supported matrix"); + } + 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, readme, security, support, dataGovernance, hosted, packageText] = await Promise.all([ + read("docs/governance-policy.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"), + ]); + const policy = JSON.parse(policyText) as unknown; + return [ + ...validateGovernancePolicy(policy), + ...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/specs/changes/platform-security-privacy-policy/change.json b/specs/changes/platform-security-privacy-policy/change.json index 86283f4..d501ad3 100644 --- a/specs/changes/platform-security-privacy-policy/change.json +++ b/specs/changes/platform-security-privacy-policy/change.json @@ -3,7 +3,7 @@ "id": "platform-security-privacy-policy", "title": "Publish platform, security, privacy, and release policy", "lane": "high-risk", - "status": "approved", + "status": "implementing", "affectedRequirements": [ "SEC-01", "SEC-10", @@ -24,7 +24,7 @@ "currentSpecs": [ "specs/current/governance-policy.spec.md" ], - "currentSpecsUpdated": false, + "currentSpecsUpdated": true, "approval": { "by": "bitgorust", "at": "2026-08-16T08:17:19Z" diff --git a/specs/changes/platform-security-privacy-policy/evidence.md b/specs/changes/platform-security-privacy-policy/evidence.md index 666abd1..53fba6e 100644 --- a/specs/changes/platform-security-privacy-policy/evidence.md +++ b/specs/changes/platform-security-privacy-policy/evidence.md @@ -1,78 +1,129 @@ # Evidence: Publish platform, security, privacy, and release policy -This draft records known evidence state; links are added only after approved artifacts exist. +This record separates policy publication from current operational evidence. Failed and +unverified results keep the packet open and block the Phase 0 gate. ## Requirement: SEC-01 -- Validation: pending accountable security review. -- Verification: no versioned threat model exists. -- Result: missing. +- 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: pending response-policy review. -- Verification: GitHub private vulnerability reporting returned `enabled: false` on 2026-08-16. +- Validation: the severity, response, disclosure, rotation and compromised-release policy was + approved on 2026-08-16, including refusal while private intake is absent. +- Verification: GitHub private vulnerability reporting returned `enabled: false` on + 2026-08-16; the public policy exposes that failure and does not solicit sensitive issues. - Result: fail; there is no verified private reporting path. +- Evidence: [@test](test/governance-policy.test.ts), + [@manual](docs/evidence/governance/provider-status-2026-08-16.md) ## Requirement: PRIV-01 -- Validation: pending privacy review. -- Verification: no versioned mode-specific data inventory exists. -- Result: missing. +- 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: pending policy review. -- Verification: current code audit and tests must prove no default telemetry after approval. -- Result: pending. +- 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: pending policy review. -- Verification: existing secret/publication tests are incomplete evidence until mapped to every output boundary. +- Validation: secret/output exclusions and narrow override wording were approved. +- Verification: existing guard and deploy tests cover content/title and staged HTML scanning, + but do not prove every future metadata/evidence/provider boundary. - Result: partial. +- Evidence: [@test](test/guard.test.ts), [@test](test/plugin.test.ts), + [@test](test/cloudflare.test.ts) ## Requirement: PRIV-04 -- Validation: pending operator disclosure review. -- Verification: target-specific operator/region/recipient disclosure is incomplete. -- Result: missing. +- 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: pending deletion-consequence review. -- Verification: mode-specific list/export/correct/delete and backup-expiry evidence is absent. -- Result: missing. +- 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: pending evidence-retention review. -- Verification: no canonical minimization and retention policy exists. -- Result: missing. +- 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: pending public abuse/IP review. -- Verification: no canonical reporting/takedown policy exists. -- Result: missing. +- 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. +- Result: partial; real operator handling and a complete asset inventory remain unverified. +- Evidence: [@manual](docs/data-governance.md), + [@manual](docs/evidence/governance/supply-chain-2026-08-16.md) ## Requirement: COMPAT-01 -- Validation: pending support-scope approval. -- Verification: OpenCode 1.18.18 has dated Ubuntu-local evidence; required macOS, Windows, browser, mobile, and second-host evidence is unavailable. +- Validation: Node 24, the target OS/browser envelope and exact-evidence promotion rule were + approved. +- Verification: OpenCode 1.18.18 has dated Linux registration evidence; the exact OS/Node/ + browser journey was not recorded. Every target cell is 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/local-environment-2026-08-16.md) ## Requirement: DIST-03 -- Validation: pending release-policy review. -- Verification: SemVer and Conventional Commit practice exist, but canonical claim/limits/evidence release policy is incomplete. -- Result: partial. +- 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: pending D-06 approval. -- Verification: publish workflow requests npm provenance; no retained SBOM, vulnerability/license output, or registry-side attestation verification exists. -- Result: partial and insufficient. +- Validation: npm trusted publishing, CycloneDX, audit/signature/license and exact-byte binding + were approved. +- Verification: npm 10 generated CycloneDX and exact tarball digests were inspected; current + npm verified 220 signatures and 22 attestations, while audit found 8 vulnerabilities. + Published 0.14.3 provenance was verified for its own tag/commit. Trusted-publisher + configuration, final-byte binding and future-candidate provenance remain unverified. +- Result: fail for production readiness. +- Evidence: [@manual](docs/evidence/governance/supply-chain-2026-08-16.md), + [@manual](docs/evidence/governance/provider-status-2026-08-16.md) ## Requirement: DIST-05 -- Validation: pending dependency/license policy review. -- Verification: lockfile exists; complete license/network/CSP/removal disposition is absent. -- Result: partial. +- Validation: license, vulnerability, network/CSP, weight, owner and removal rules were + approved. +- Verification: the lockfile inventory has one missing license entry and `npm audit` reports + seven high and one moderate finding; complete runtime disposition is absent. +- Result: fail for production readiness. +- Evidence: [@manual](docs/evidence/governance/supply-chain-2026-08-16.md), + [@test](test/render.test.ts) ## Requirement: DIST-06 -- Validation: pending support-window approval. -- Verification: supported-version, deprecation, vulnerability-response, and end-of-life policy is absent. -- Result: missing. +- 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: pending attribution policy review. -- Verification: repository license exists; a complete documentation/assets/font/reference inventory is absent. -- Result: partial. +- Validation: redistribution authority, attribution and private-reference handling were + approved. +- Verification: repository MIT license exists, but the lockfile has one missing license entry + and the complete docs/assets/fonts/reference disposition is absent. +- Result: fail for a release readiness claim. +- Evidence: [@manual](LICENSE), + [@manual](docs/evidence/governance/supply-chain-2026-08-16.md) diff --git a/specs/changes/platform-security-privacy-policy/tasks.md b/specs/changes/platform-security-privacy-policy/tasks.md index f08f085..9ce17bc 100644 --- a/specs/changes/platform-security-privacy-policy/tasks.md +++ b/specs/changes/platform-security-privacy-policy/tasks.md @@ -1,12 +1,12 @@ # Tasks: Publish platform, security, privacy, and release policy -- [ ] Confirm proposal validation and human approval. -- [ ] Publish canonical support/D-01/D-06, security/threat, data-governance, and public vulnerability policies. -- [ ] Add deterministic policy completeness, matrix-evidence, inventory, and claim-consistency tests. -- [ ] Generate and inspect CycloneDX, vulnerability, signature, license, and packed-byte outputs. +- [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. - [ ] Run every claimed host/browser cell and retain exact real results; leave unavailable cells unverified. - [ ] Obtain explicit authority before enabling GitHub private vulnerability reporting or changing npm trusted publishing. -- [ ] Verify external reporting/provenance state rather than inferring it from configuration. -- [ ] Record validation and verification evidence. -- [ ] Add `specs/current/governance-policy.spec.md` and reconcile README, roadmap, traceability, hosting, comparison, and release template. +- [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. - [ ] Run repository validation and archive the packet. diff --git a/specs/current/governance-policy.spec.md b/specs/current/governance-policy.spec.md new file mode 100644 index 0000000..564d334 --- /dev/null +++ b/specs/current/governance-policy.spec.md @@ -0,0 +1,34 @@ +# 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. There are zero supported complete cells; all target cells are unverified. +- `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 disabled, so sensitive private intake + is unavailable and production readiness fails. Severity/response, rotation and compromised- + release processes are published for use after an accountable private path is operational. +- `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. +- `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 then-current published release for its + own tag/commit; trusted- + publisher configuration and future-candidate provenance are not. Current audit, signature, + license, provider and platform evidence fails or is incomplete, so no production-readiness + claim is permitted. diff --git a/test/governance-policy.test.ts b/test/governance-policy.test.ts new file mode 100644 index 0000000..a12da0a --- /dev/null +++ b/test/governance-policy.test.ts @@ -0,0 +1,49 @@ +import { readFile } from "node:fs/promises"; +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + validateGovernanceClaims, + validateGovernancePolicy, + validateGovernanceRepository, +} 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; + +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, /unavailable private reporting/); +}); From 1b0e2987c802839f8c4bc4ab99415e48c1089c83 Mon Sep 17 00:00:00 2001 From: bitgorust Date: Sun, 16 Aug 2026 12:30:43 +0200 Subject: [PATCH 06/18] ci: gate exact release supply chain --- .github/workflows/publish.yml | 36 ++++- .../governance/supply-chain-2026-08-16.md | 17 ++- docs/support-policy.md | 5 + package.json | 1 + scripts/release-integrity.ts | 136 ++++++++++++++++++ .../evidence.md | 12 +- specs/current/governance-policy.spec.md | 7 +- test/release-integrity.test.ts | 58 ++++++++ 8 files changed, 260 insertions(+), 12 deletions(-) create mode 100644 scripts/release-integrity.ts create mode 100644 test/release-integrity.test.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index dcf1e56..e1a68e9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,6 +23,40 @@ jobs: - run: npm run build - run: npm test - run: npm run check - - run: npm publish + - name: Generate and gate supply-chain evidence + shell: bash + run: | + mkdir -p release-evidence + 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 > release-evidence/licenses.json + license_status=$? + set -e + sha256sum release-evidence/* + test "$sbom_status" -eq 0 + test "$audit_status" -eq 0 + test "$signature_status" -eq 0 + test "$license_status" -eq 0 + - name: Pack exact release bytes + id: pack + shell: bash + run: | + npm pack --json > release-evidence/pack.json + node scripts/release-integrity.ts pack-output release-evidence/pack.json package.json >> "$GITHUB_OUTPUT" + - 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 diff --git a/docs/evidence/governance/supply-chain-2026-08-16.md b/docs/evidence/governance/supply-chain-2026-08-16.md index 8b08163..fc669b0 100644 --- a/docs/evidence/governance/supply-chain-2026-08-16.md +++ b/docs/evidence/governance/supply-chain-2026-08-16.md @@ -80,7 +80,16 @@ 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. Until its package metadata/redistribution -authority and the alternative-license selections are reviewed and recorded, the license gate -fails. The repository source is MIT; full documentation/example/asset/reference attribution -still requires release-level inspection. +`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. diff --git a/docs/support-policy.md b/docs/support-policy.md index 1b3a431..0e8f901 100644 --- a/docs/support-policy.md +++ b/docs/support-policy.md @@ -77,6 +77,11 @@ publicly verified, and that prior attestation cannot cover future candidate byte still needs its own post-publish 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. No tag was +created or release attempted by this policy change. + 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 diff --git a/package.json b/package.json index d779de8..052029a 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "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", "study": "node scripts/journey-study.ts", "spec": "node scripts/spec-workflow.ts", "prepare": "git config core.hooksPath .githooks || true", diff --git a/scripts/release-integrity.ts b/scripts/release-integrity.ts new file mode 100644 index 0000000..c14d250 --- /dev/null +++ b/scripts/release-integrity.ts @@ -0,0 +1,136 @@ +#!/usr/bin/env node + +import { readFile } from "node:fs/promises"; + +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): 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++; + if (!isRecord(detail) || typeof detail["license"] !== "string" || detail["license"].trim() === "") { + missingLicenseEntries.push(path); + licenses["MISSING"] = (licenses["MISSING"] ?? 0) + 1; + continue; + } + const license = detail["license"]; + 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): string[] { + const errors: string[] = []; + if (inventory.missingLicenseEntries.length > 0) { + errors.push(`missing license metadata: ${inventory.missingLicenseEntries.join(", ")}`); + } + if (inventory.reviewRequired.length > 0) { + errors.push( + `compound licenses need an explicit branch disposition: ${inventory.reviewRequired + .map((entry) => `${entry.path} (${entry.license})`) + .join(", ")}`, + ); + } + return errors; +} + +export interface PackCoordinate { + filename: string; + integrity: string; + shasum: string; + packageSpec: string; +} + +export function packCoordinate(pack: unknown, packageJson: unknown): PackCoordinate { + if (!Array.isArray(pack) || pack.length !== 1 || !isRecord(pack[0])) { + throw new Error("npm pack JSON must contain exactly one result"); + } + if (!isRecord(packageJson)) throw new Error("package.json must be an object"); + const result = pack[0]; + 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 function verifyPublishedDistribution(pack: unknown, dist: unknown): string[] { + const errors: string[] = []; + if (!isRecord(pack) || !isRecord(dist)) return ["pack coordinate and registry dist must be objects"]; + for (const field of ["integrity", "shasum"]) { + if (typeof pack[field] !== "string" || dist[field] !== pack[field]) { + errors.push(`registry ${field} does not match the packed bytes`); + } + } + const attestations = dist["attestations"]; + if (!isRecord(attestations) || !isRecord(attestations["provenance"]) || + typeof attestations["provenance"]["predicateType"] !== "string") { + errors.push("registry provenance attestation is missing"); + } + if (!Array.isArray(dist["signatures"]) || dist["signatures"].length === 0) { + errors.push("registry package signature is missing"); + } + return errors; +} + +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 === 1) { + const inventory = createLicenseInventory(await json(args[0])); + console.log(JSON.stringify(inventory, null, 2)); + const errors = licenseInventoryErrors(inventory); + 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 === "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; + } + console.error("Usage: release-integrity.ts licenses | pack-output | verify-registry "); + process.exitCode = 2; +} + +if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) await main(); diff --git a/specs/changes/platform-security-privacy-policy/evidence.md b/specs/changes/platform-security-privacy-policy/evidence.md index 53fba6e..ca1e70a 100644 --- a/specs/changes/platform-security-privacy-policy/evidence.md +++ b/specs/changes/platform-security-privacy-policy/evidence.md @@ -97,19 +97,23 @@ unverified results keep the packet open and block the Phase 0 gate. - Verification: npm 10 generated CycloneDX and exact tarball digests were inspected; current npm verified 220 signatures and 22 attestations, while audit found 8 vulnerabilities. Published 0.14.3 provenance was verified for its own tag/commit. Trusted-publisher - configuration, final-byte binding and future-candidate provenance remain unverified. + configuration and future-candidate provenance remain unverified. The tag workflow now + fails closed on all four prepublish outputs, publishes one coordinated tarball, and verifies + registry bytes/signature/provenance after publish. - Result: fail for production readiness. - Evidence: [@manual](docs/evidence/governance/supply-chain-2026-08-16.md), - [@manual](docs/evidence/governance/provider-status-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 lockfile inventory has one missing license entry and `npm audit` reports - seven high and one moderate finding; complete runtime disposition is absent. + seven high and one moderate finding; two compound-license branches also lack explicit + selection, and complete runtime disposition is absent. - Result: fail for production readiness. - Evidence: [@manual](docs/evidence/governance/supply-chain-2026-08-16.md), - [@test](test/render.test.ts) + [@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 diff --git a/specs/current/governance-policy.spec.md b/specs/current/governance-policy.spec.md index 564d334..4ef5629 100644 --- a/specs/current/governance-policy.spec.md +++ b/specs/current/governance-policy.spec.md @@ -29,6 +29,7 @@ publishing/provenance, npm CycloneDX, audit/signatures, license disposition and exact packed- byte binding. The dated provider report verifies the then-current published release for its own tag/commit; trusted- - publisher configuration and future-candidate provenance are not. Current audit, signature, - license, provider and platform evidence fails or is incomplete, so no production-readiness - claim is permitted. + publisher configuration and future-candidate provenance are not. Current audit, license, + provider and platform evidence fails or is incomplete, so no production-readiness + claim is permitted. The tag workflow fails closed on prepublish evidence, publishes the + coordinated tarball, and verifies registry integrity/signature/provenance afterward. diff --git a/test/release-integrity.test.ts b/test/release-integrity.test.ts new file mode 100644 index 0000000..aa90502 --- /dev/null +++ b/test/release-integrity.test.ts @@ -0,0 +1,58 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + createLicenseInventory, + licenseInventoryErrors, + packCoordinate, + verifyPublishedDistribution, +} 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("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", + }, + ); +}); + +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, { integrity: "wrong", shasum: "def", signatures: [] }), + [ + "registry integrity does not match the packed bytes", + "registry provenance attestation is missing", + "registry package signature is missing", + ], + ); +}); From a6f983c2b8b78167f45f82908390249d4ce7d2af Mon Sep 17 00:00:00 2001 From: bitgorust Date: Sun, 16 Aug 2026 16:54:40 +0200 Subject: [PATCH 07/18] docs: propose dependency remediation --- ...endency-remediation-proposal-2026-08-16.md | 51 +++++++ .../change.json | 31 ++++ .../delta.md | 143 ++++++++++++++++++ .../design.md | 68 +++++++++ .../evidence.md | 46 ++++++ .../proposal.md | 47 ++++++ .../tasks.md | 14 ++ 7 files changed, 400 insertions(+) create mode 100644 docs/evidence/governance/dependency-remediation-proposal-2026-08-16.md create mode 100644 specs/changes/supply-chain-vulnerability-remediation/change.json create mode 100644 specs/changes/supply-chain-vulnerability-remediation/delta.md create mode 100644 specs/changes/supply-chain-vulnerability-remediation/design.md create mode 100644 specs/changes/supply-chain-vulnerability-remediation/evidence.md create mode 100644 specs/changes/supply-chain-vulnerability-remediation/proposal.md create mode 100644 specs/changes/supply-chain-vulnerability-remediation/tasks.md 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..21ddb84 --- /dev/null +++ b/docs/evidence/governance/dependency-remediation-proposal-2026-08-16.md @@ -0,0 +1,51 @@ +# Dependency remediation proposal — 2026-08-16 + +Status: proposal only; no dependency or license disposition has been approved or applied. + +## 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. Every selection remains pending maintainer approval. + +| 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 | +| `mermaid@11.16.1 > dompurify@3.4.13` | `node_modules/dompurify/LICENSE`, SHA-256 `cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30` | Apache-2.0 | +| `@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 | + +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 project disposition without human review. + +## 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/specs/changes/supply-chain-vulnerability-remediation/change.json b/specs/changes/supply-chain-vulnerability-remediation/change.json new file mode 100644 index 0000000..b778931 --- /dev/null +++ b/specs/changes/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": "draft", + "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": false, + "approval": { + "by": "", + "at": "" + }, + "withdrawal": { + "by": "", + "at": "", + "reason": "" + }, + "createdAt": "2026-08-16", + "archivedAt": null +} diff --git a/specs/changes/supply-chain-vulnerability-remediation/delta.md b/specs/changes/supply-chain-vulnerability-remediation/delta.md new file mode 100644 index 0000000..9b9c6ad --- /dev/null +++ b/specs/changes/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/changes/supply-chain-vulnerability-remediation/design.md b/specs/changes/supply-chain-vulnerability-remediation/design.md new file mode 100644 index 0000000..409bb15 --- /dev/null +++ b/specs/changes/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/changes/supply-chain-vulnerability-remediation/evidence.md b/specs/changes/supply-chain-vulnerability-remediation/evidence.md new file mode 100644 index 0000000..4c307f8 --- /dev/null +++ b/specs/changes/supply-chain-vulnerability-remediation/evidence.md @@ -0,0 +1,46 @@ +# Evidence: Remediate renderer dependency vulnerabilities and licenses + +This packet is a proposal. The current audit and license failures are retained rather than +reported as passing implementation evidence. + +## Requirement: RENDER-02 + +- Validation: Current chart support is public behavior, so the major upgrade must preserve every documented chart kind. +- Verification: Pending approved implementation, canonical chart tests, and representative browser smoke evidence. +- Result: fail — the vulnerable current runtime family is not an acceptable release candidate. +- Evidence: [@manual](docs/evidence/governance/dependency-remediation-proposal-2026-08-16.md) + +## Requirement: SEC-02 + +- Validation: The cited exploit classes process user-controlled chart data at an existing untrusted boundary. +- Verification: Pending adversarial tooltip, expression, data-mutation, encoding, and global-authority tests. +- Result: fail — remediation tests and upgraded runtimes are not implemented. +- Evidence: [@manual](docs/evidence/governance/dependency-remediation-proposal-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: Pending CSP, offline-network, interpreter-path, credential-isolation, and browser-global tests. +- Result: fail — the proposed major integration has not been verified. +- Evidence: [@manual](docs/evidence/governance/dependency-remediation-proposal-2026-08-16.md) + +## Requirement: DIST-04 + +- Validation: Release CI already blocks on audit and license findings, exposing the exact remediation need. +- Verification: Pending clean exact-candidate audit, SBOM, license, integrity, signature, and provenance evidence. +- Result: fail — `npm run check:licenses` and the dependency audit currently report unresolved findings. +- Evidence: [@manual](docs/evidence/governance/dependency-remediation-proposal-2026-08-16.md) + +## Requirement: DIST-05 + +- Validation: Major renderer upgrades and alternative license branches require explicit compatibility and maintainer review. +- Verification: Pending approved hash-bound dispositions, lockfile update, compatibility tests, size delta, and package-removal smoke. +- Result: fail — no proposed license branch is approved and no dependency was changed. +- Evidence: [@manual](docs/evidence/governance/dependency-remediation-proposal-2026-08-16.md) + +## Requirement: QUAL-06 + +- Validation: The advisory payload classes refine the existing script-breakout and resource-exhaustion threat requirements. +- Verification: Pending table-driven gate-model and renderer adversarial regression tests. +- Result: fail — the new threat fixtures do not yet exist. +- Evidence: [@model](test/release-integrity.test.ts) diff --git a/specs/changes/supply-chain-vulnerability-remediation/proposal.md b/specs/changes/supply-chain-vulnerability-remediation/proposal.md new file mode 100644 index 0000000..e58e3ae --- /dev/null +++ b/specs/changes/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/changes/supply-chain-vulnerability-remediation/tasks.md b/specs/changes/supply-chain-vulnerability-remediation/tasks.md new file mode 100644 index 0000000..0948b9f --- /dev/null +++ b/specs/changes/supply-chain-vulnerability-remediation/tasks.md @@ -0,0 +1,14 @@ +# Tasks: Remediate renderer dependency vulnerabilities and licenses + +- [ ] Confirm proposal validation and human approval. +- [ ] Record approved license branches and bind them to exact installed license-file hashes. +- [ ] Upgrade the ECharts and Vega renderer family together and update the lockfile. +- [ ] Add compatibility coverage for every documented chart kind and canonical example. +- [ ] Add adversarial ECharts tooltip and Vega expression, data-mutation, and global-gadget tests. +- [ ] Verify strict CSP, interpreter-only Vega execution, no view-time network, and no new globals. +- [ ] Verify representative browser rendering, accessibility behavior, and runtime/package-size deltas. +- [ ] Verify old self-contained pages remain usable after package removal. +- [ ] Generate exact-candidate audit, license, signature, SBOM, provenance, and integrity evidence. +- [ ] Update `specs/current/governance-policy.spec.md` and add + `specs/current/renderer-dependencies.spec.md`. +- [ ] Reconcile documentation, run repository validation, verify all evidence, and archive the packet. From 0bf6798ac436c858a3cc2535744753a4bd2ee7f0 Mon Sep 17 00:00:00 2001 From: bitgorust Date: Sun, 16 Aug 2026 17:48:58 +0200 Subject: [PATCH 08/18] fix: remediate renderer dependency vulnerabilities --- .github/workflows/ci.yml | 21 +- .github/workflows/publish.yml | 21 +- ...endency-remediation-proposal-2026-08-16.md | 13 +- .../renderer-remediation-2026-08-16.md | 129 +++ .../renderer-upgrade-chromium-2026-08-16.png | Bin 0 -> 94505 bytes .../governance/supply-chain-2026-08-16.md | 7 + docs/license-dispositions.json | 32 + docs/roadmap.md | 2 +- package-lock.json | 827 ++++++++---------- package.json | 10 +- scripts/release-integrity.ts | 306 ++++++- .../evidence.md | 30 +- .../change.json | 8 +- .../evidence.md | 54 +- .../tasks.md | 18 +- specs/current/governance-policy.spec.md | 10 +- specs/current/renderer-dependencies.spec.md | 32 + src/render.ts | 4 +- test/release-integrity.test.ts | 122 +++ test/renderer-security.test.ts | 141 +++ 20 files changed, 1223 insertions(+), 564 deletions(-) create mode 100644 docs/evidence/governance/renderer-remediation-2026-08-16.md create mode 100644 docs/evidence/governance/renderer-upgrade-chromium-2026-08-16.png create mode 100644 docs/license-dispositions.json create mode 100644 specs/current/renderer-dependencies.spec.md create mode 100644 test/renderer-security.test.ts 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 e1a68e9..873db80 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,10 +23,16 @@ jobs: - run: npm run build - run: npm test - run: npm run check - - name: Generate and gate supply-chain evidence + - 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=$? @@ -34,20 +40,17 @@ jobs: audit_status=$? npm audit signatures > release-evidence/signatures.txt signature_status=$? - node scripts/release-integrity.ts licenses package-lock.json > release-evidence/licenses.json + 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/* + 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 - - name: Pack exact release bytes - id: pack - shell: bash - run: | - npm pack --json > release-evidence/pack.json - node scripts/release-integrity.ts pack-output release-evidence/pack.json package.json >> "$GITHUB_OUTPUT" + test "$provenance_status" -eq 0 - name: Publish exact tarball shell: bash run: | diff --git a/docs/evidence/governance/dependency-remediation-proposal-2026-08-16.md b/docs/evidence/governance/dependency-remediation-proposal-2026-08-16.md index 21ddb84..c9bdbc5 100644 --- a/docs/evidence/governance/dependency-remediation-proposal-2026-08-16.md +++ b/docs/evidence/governance/dependency-remediation-proposal-2026-08-16.md @@ -1,6 +1,7 @@ # Dependency remediation proposal — 2026-08-16 -Status: proposal only; no dependency or license disposition has been approved or applied. +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 @@ -27,18 +28,18 @@ scope because artifact chart specifications are untrusted user input: 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. Every selection remains pending maintainer approval. +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 | -| `mermaid@11.16.1 > dompurify@3.4.13` | `node_modules/dompurify/LICENSE`, SHA-256 `cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30` | Apache-2.0 | -| `@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 | +| `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 project disposition without human review. +convert that preference into a broader approval for future versions, hashes, or branches. ## Required acceptance evidence 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..70803a1 --- /dev/null +++ b/docs/evidence/governance/renderer-remediation-2026-08-16.md @@ -0,0 +1,129 @@ +# Renderer dependency remediation — 2026-08-16 + +Status: implementation checkpoint. Local candidate gates pass; exact-commit GitHub Actions +candidate provenance and artifact retention are pending the first pushed implementation commit. +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 comparing this local digest to a later run. + +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`. + +## 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 now 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. The local checkpoint cannot prove the +future exact-commit CI run or npm registry provenance; those remain 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`, while the exact same Node version passed the full suite in + the preceding GitHub Actions run. The pushed exact-commit run remains the authoritative full- + suite verdict for this checkpoint. +- 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 0000000000000000000000000000000000000000..04a603bcc6557d302e17bef2f6bd5b85cee50437 GIT binary patch literal 94505 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%0AT%ed=ptmnYg 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

Q@ zlE)0hvniV++Ln4JK;CWWUx9pd0eXHLTW=H^8_213VtmBdnvt6qRgXZ^ObXy1&RR<% zbp6XsUZY4`5hnqTRG)onX^U#}9azK$Pws`jZA`YaV!S8oHL~$$y500^70F6!+ES#y z>IH@ND^GAMy)@hH+~4IiK6K<&9LlfM95;T*@bMI)

$t7m}wO!`3Sw^uKx=Hm(}R2R-f3?<-d9 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 { return typeof value === "object" && value !== null && !Array.isArray(value); @@ -63,6 +64,211 @@ const REQUIRED_BOUNDARIES = [ "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"]; @@ -250,18 +456,22 @@ export function validateGovernanceClaims(inputs: GovernanceClaimInputs): string[ export async function validateGovernanceRepository(root: string): Promise { const read = (path: string): Promise => readFile(join(root, path), "utf8"); try { - const [policyText, readme, security, support, dataGovernance, hosted, packageText] = await Promise.all([ + 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, diff --git a/specs/changes/platform-security-privacy-policy/evidence.md b/specs/changes/platform-security-privacy-policy/evidence.md index f489023..1430b87 100644 --- a/specs/changes/platform-security-privacy-policy/evidence.md +++ b/specs/changes/platform-security-privacy-policy/evidence.md @@ -68,10 +68,14 @@ unverified results keep the packet open and block the Phase 0 gate. - 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. -- Result: partial; real operator handling and a complete asset inventory remain unverified. + 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/supply-chain-2026-08-16.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 @@ -129,9 +133,11 @@ unverified results keep the packet open and block the Phase 0 gate. ## Requirement: DIST-07 - Validation: redistribution authority, attribution and private-reference handling were approved. -- Verification: repository MIT license exists and runtime dependency disposition now passes, - but complete docs/assets/fonts/reference disposition is absent. -- Result: fail for a release readiness claim. +- 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/supply-chain-2026-08-16.md), - [@manual](docs/evidence/governance/renderer-remediation-2026-08-16.md) + [@manual](docs/evidence/governance/redistribution-2026-08-16.md), + [@test](test/governance-policy.test.ts) diff --git a/specs/current/governance-policy.spec.md b/specs/current/governance-policy.spec.md index a7a521f..4c55422 100644 --- a/specs/current/governance-policy.spec.md +++ b/specs/current/governance-policy.spec.md @@ -23,7 +23,9 @@ - `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. + backups, SLA, legal compliance and project-operated hosting are not claimed. 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- @@ -31,7 +33,8 @@ own tag/commit; trusted- publisher configuration and future registry provenance are not. The approved renderer remediation clears the current audit and hash-bound license gates and adds exact candidate - SBOM/provenance artifact retention. Provider and platform evidence remains incomplete, so no - production-readiness claim is permitted. The tag workflow fails closed on prepublish - evidence, publishes the coordinated tarball, and verifies registry integrity/signature/ - provenance afterward. + 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 claim is permitted. The tag workflow fails closed on + prepublish evidence, publishes the coordinated tarball, and verifies registry integrity/ + signature/provenance afterward. diff --git a/test/governance-policy.test.ts b/test/governance-policy.test.ts index a12da0a..0d4695c 100644 --- a/test/governance-policy.test.ts +++ b/test/governance-policy.test.ts @@ -5,10 +5,20 @@ 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), []); @@ -47,3 +57,24 @@ test("inflated README provenance and mismatched Node claims fail consistency", ( assert.match(errors, /engines.node must match/); assert.match(errors, /unavailable private reporting/); }); + +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/); +}); From 175c165531b91c40c987f569e9be36748eeecf32 Mon Sep 17 00:00:00 2001 From: bitgorust Date: Sun, 16 Aug 2026 18:15:19 +0200 Subject: [PATCH 11/18] docs: record exact Ubuntu packed observation --- .../local-environment-2026-08-16.md | 8 ++ .../ubuntu-packed-observation-2026-08-16.md | 86 +++++++++++++++++++ docs/governance-policy.json | 12 +-- docs/support-policy.md | 14 +-- .../evidence.md | 9 +- specs/current/governance-policy.spec.md | 4 +- 6 files changed, 115 insertions(+), 18 deletions(-) create mode 100644 docs/evidence/governance/ubuntu-packed-observation-2026-08-16.md diff --git a/docs/evidence/governance/local-environment-2026-08-16.md b/docs/evidence/governance/local-environment-2026-08-16.md index 239850e..2e8699c 100644 --- a/docs/evidence/governance/local-environment-2026-08-16.md +++ b/docs/evidence/governance/local-environment-2026-08-16.md @@ -15,3 +15,11 @@ The system shell is below the Node 24 support floor, the local Node 24 binary is 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/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/governance-policy.json b/docs/governance-policy.json index a016083..ad1c735 100644 --- a/docs/governance-policy.json +++ b/docs/governance-policy.json @@ -29,13 +29,13 @@ "id": "observed-linux-opencode", "class": "tested", "status": "tested", - "os": "Linux x64 (distribution not recorded)", - "node": "not recorded by the host probe", + "os": "Ubuntu 24.04.4 LTS, Linux 6.8.0-106-generic, x86_64", + "node": "24.19.0", "opencode": "1.18.18", - "browser": "not exercised", - "scope": "Checkout and registry-package plugin registration only", - "testedAt": "2026-08-15", - "evidence": "docs/evidence/opencode-host-verification.md" + "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", diff --git a/docs/support-policy.md b/docs/support-policy.md index 0e8f901..8551944 100644 --- a/docs/support-policy.md +++ b/docs/support-policy.md @@ -7,16 +7,16 @@ The machine-readable status record is [`governance-policy.json`](governance-poli ## Current support status -There are currently **no fully supported platform/browser cells**. The exact stable OpenCode -version in the dated host probe was tested for plugin registration on Linux x64, but that -probe did not record the distribution, -Node version, clean user journey, or browser scope required for support. CI separately runs -repository checks on Node 24/Ubuntu. Those observations cannot be combined into one platform -pass. The target matrix therefore remains unverified and blocks a production-readiness claim. +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. | Cell | Target | Current status | Evidence or missing scope | |---|---|---|---| -| OpenCode host observation | Linux x64, exact version in dated probe | Tested, not supported | [Host probe](evidence/opencode-host-verification.md); distribution/Node/browser/journey absent | +| 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 | diff --git a/specs/changes/platform-security-privacy-policy/evidence.md b/specs/changes/platform-security-privacy-policy/evidence.md index 1430b87..94d20ca 100644 --- a/specs/changes/platform-security-privacy-policy/evidence.md +++ b/specs/changes/platform-security-privacy-policy/evidence.md @@ -80,12 +80,13 @@ unverified results keep the packet open and block the Phase 0 gate. ## Requirement: COMPAT-01 - Validation: Node 24, the target OS/browser envelope and exact-evidence promotion rule were approved. -- Verification: OpenCode 1.18.18 has dated Linux registration evidence; the exact OS/Node/ - browser journey was not recorded. Every target cell is unverified and there are zero - supported complete cells. +- 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/local-environment-2026-08-16.md) + [@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 diff --git a/specs/current/governance-policy.spec.md b/specs/current/governance-policy.spec.md index 4c55422..058f20a 100644 --- a/specs/current/governance-policy.spec.md +++ b/specs/current/governance-policy.spec.md @@ -14,7 +14,9 @@ - `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. There are zero supported complete cells; all target cells are unverified. + 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. - `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 disabled, so sensitive private intake From a91b693ace77b1472adef34fa84eab425df0b2cd Mon Sep 17 00:00:00 2001 From: bitgorust Date: Sun, 16 Aug 2026 18:21:09 +0200 Subject: [PATCH 12/18] fix: scan complete deployment staging --- docs/data-governance.md | 5 +++ docs/threat-model.md | 11 ++++--- .../evidence.md | 10 ++++-- specs/current/governance-policy.spec.md | 3 ++ src/cli.ts | 8 ++++- src/cloudflare-publisher.ts | 14 ++++++++ src/github-pages.ts | 14 ++++++++ src/guard.ts | 22 ++++++++++++- src/plugin.ts | 2 ++ test/cli.test.ts | 20 ++++++++++++ test/cloudflare.test.ts | 30 ++++++++++++++++- test/github-pages.test.ts | 23 +++++++++++++ test/guard.test.ts | 25 ++++++++++++--- test/plugin.test.ts | 32 +++++++++++++++++++ 14 files changed, 205 insertions(+), 14 deletions(-) diff --git a/docs/data-governance.md b/docs/data-governance.md index af15bfe..ec81657 100644 --- a/docs/data-governance.md +++ b/docs/data-governance.md @@ -60,6 +60,11 @@ trees, browser configuration, diagnostics, exports, or public evidence. Redact o 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 diff --git a/docs/threat-model.md b/docs/threat-model.md index b051c09..dea6523 100644 --- a/docs/threat-model.md +++ b/docs/threat-model.md @@ -60,8 +60,10 @@ boundaries describe required future work and are never evidence of shipped prote 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, final artifact scanning, fixed argument - arrays, state-directory exclusion, scoped Worker KV names, and staged-deploy unit tests. +- 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. @@ -70,8 +72,9 @@ boundaries describe required future work and are never evidence of shipped prote - 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, secret scan precedes deploy, GitHub state dirs are - excluded, and policies disclose operator/removal limitations. +- 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. diff --git a/specs/changes/platform-security-privacy-policy/evidence.md b/specs/changes/platform-security-privacy-policy/evidence.md index 94d20ca..3cbbd1d 100644 --- a/specs/changes/platform-security-privacy-policy/evidence.md +++ b/specs/changes/platform-security-privacy-policy/evidence.md @@ -37,10 +37,14 @@ unverified results keep the packet open and block the Phase 0 gate. ## Requirement: PRIV-03 - Validation: secret/output exclusions and narrow override wording were approved. -- Verification: existing guard and deploy tests cover content/title and staged HTML scanning, - but do not prove every future metadata/evidence/provider boundary. -- Result: partial. +- 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 diff --git a/specs/current/governance-policy.spec.md b/specs/current/governance-policy.spec.md index 058f20a..0ff673b 100644 --- a/specs/current/governance-policy.spec.md +++ b/specs/current/governance-policy.spec.md @@ -26,6 +26,9 @@ 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 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/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/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/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/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); From 21f90b8c5578cbfc774ce0bcfc59ea999792b589 Mon Sep 17 00:00:00 2001 From: bitgorust Date: Sun, 16 Aug 2026 18:41:17 +0200 Subject: [PATCH 13/18] docs: verify private vulnerability intake --- README.md | 3 ++- SECURITY.md | 17 +++++++++-------- .../governance/provider-status-2026-08-16.md | 8 +++++--- docs/governance-policy.json | 4 ++-- docs/security.md | 16 ++++++++-------- scripts/governance-policy.ts | 4 ++-- .../evidence.md | 9 ++++++--- specs/current/governance-policy.spec.md | 7 ++++--- test/governance-policy.test.ts | 2 +- 9 files changed, 39 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index b3f5120..5673210 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,8 @@ Current policy is explicit about incomplete 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 unavailable; + 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. diff --git a/SECURITY.md b/SECURITY.md index 7b383a5..83b0674 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,16 +2,17 @@ ## Reporting status -Private vulnerability reporting is currently **unavailable**. GitHub private vulnerability -reporting was checked on 2026-08-16 and was disabled. Do not place exploit details, secrets, -private artifact content, or personal data in a public issue. -GitHub documents that its private report form exists only after repository owners -[enable private vulnerability reporting](https://docs.github.com/en/code-security/how-tos/report-and-fix-vulnerabilities/configure-vulnerability-reporting/configure-for-a-repository). +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. Until an authorized maintainer enables and -verifies private reporting, this repository does not claim a safe intake path for sensitive -vulnerability reports and does not meet its production-readiness security gate. +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 diff --git a/docs/evidence/governance/provider-status-2026-08-16.md b/docs/evidence/governance/provider-status-2026-08-16.md index c9d527d..bddc100 100644 --- a/docs/evidence/governance/provider-status-2026-08-16.md +++ b/docs/evidence/governance/provider-status-2026-08-16.md @@ -4,9 +4,11 @@ Repository: `bitgorust/opencode-artifacts` | Prerequisite | Observed result | Consequence | |---|---|---| -| GitHub private vulnerability reporting | Repository API returned `private_vulnerability_reporting.enabled: false` | Failed; `SECURITY.md` cannot direct sensitive reports to a verified private path | +| 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 / future provenance | Provider configuration is not publicly verified and no future candidate is published | Unverified; prior provenance and workflow configuration cannot pass a future release | -No provider setting was changed during this observation. Enabling either setting requires an -explicitly authorized external-state checkpoint and follow-up verification. +The GitHub setting was changed only after explicit authorization on 2026-08-16 and was then +read back from the provider. npm trusted-publisher configuration was not changed in this +observation; it still requires an authenticated package-owner session and follow-up publish +verification. diff --git a/docs/governance-policy.json b/docs/governance-policy.json index ad1c735..ea8fd55 100644 --- a/docs/governance-policy.json +++ b/docs/governance-policy.json @@ -11,10 +11,10 @@ "providerPrerequisites": [ { "id": "github-private-vulnerability-reporting", - "status": "failed", + "status": "pass", "checkedAt": "2026-08-16", "evidence": "docs/evidence/governance/provider-status-2026-08-16.md", - "claim": "Private vulnerability reporting is disabled; sensitive report intake is unavailable." + "claim": "Private vulnerability reporting is enabled and verified; sensitive reports use the repository's private advisory path." }, { "id": "npm-trusted-publishing", diff --git a/docs/security.md b/docs/security.md index 37d5456..c411283 100644 --- a/docs/security.md +++ b/docs/security.md @@ -3,25 +3,25 @@ Policy version: 1. Last reviewed: 2026-08-16. The versioned [threat model](threat-model.md) and public [reporting policy](../SECURITY.md) -govern security claims. Current status is deliberately not production-ready: GitHub private -vulnerability reporting was checked and is disabled, so there is no verified private intake -path for sensitive reports. +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 private intake is operational | +| 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. They become operational only when private -intake is enabled and a release names 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. +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 diff --git a/scripts/governance-policy.ts b/scripts/governance-policy.ts index d6d07d4..999d4d2 100644 --- a/scripts/governance-policy.ts +++ b/scripts/governance-policy.ts @@ -434,8 +434,8 @@ export function validateGovernanceClaims(inputs: GovernanceClaimInputs): string[ 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.security.includes("Private vulnerability reporting is currently **unavailable**")) { - errors.push("SECURITY.md must expose unavailable private reporting"); + 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"); diff --git a/specs/changes/platform-security-privacy-policy/evidence.md b/specs/changes/platform-security-privacy-policy/evidence.md index 3cbbd1d..8b71e0b 100644 --- a/specs/changes/platform-security-privacy-policy/evidence.md +++ b/specs/changes/platform-security-privacy-policy/evidence.md @@ -14,9 +14,12 @@ unverified results keep the packet open and block the Phase 0 gate. ## 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: GitHub private vulnerability reporting returned `enabled: false` on - 2026-08-16; the public policy exposes that failure and does not solicit sensitive issues. -- Result: fail; there is no verified private reporting path. +- 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) diff --git a/specs/current/governance-policy.spec.md b/specs/current/governance-policy.spec.md index 0ff673b..6541839 100644 --- a/specs/current/governance-policy.spec.md +++ b/specs/current/governance-policy.spec.md @@ -19,9 +19,10 @@ unverified. - `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 disabled, so sensitive private intake - is unavailable and production readiness fails. Severity/response, rotation and compromised- - release processes are published for use after an accountable private path is operational. +- `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, diff --git a/test/governance-policy.test.ts b/test/governance-policy.test.ts index 0d4695c..8be6108 100644 --- a/test/governance-policy.test.ts +++ b/test/governance-policy.test.ts @@ -55,7 +55,7 @@ test("inflated README provenance and mismatched Node claims fail consistency", ( }).join("\n"); assert.match(errors, /claims provenance attestations/); assert.match(errors, /engines.node must match/); - assert.match(errors, /unavailable private reporting/); + assert.match(errors, /verified private reporting/); }); test("redistribution inventory binds every retained binary and keeps official references link-only", () => { From 57bf0cbdb0e6fd5b456246728757c17bed23c689 Mon Sep 17 00:00:00 2001 From: bitgorust Date: Sun, 16 Aug 2026 18:48:31 +0200 Subject: [PATCH 14/18] docs: propose public preview release gate --- .../public-preview-release-gate/change.json | 31 ++++ .../public-preview-release-gate/delta.md | 164 ++++++++++++++++++ .../public-preview-release-gate/design.md | 74 ++++++++ .../public-preview-release-gate/evidence.md | 40 +++++ .../public-preview-release-gate/proposal.md | 55 ++++++ .../public-preview-release-gate/tasks.md | 10 ++ 6 files changed, 374 insertions(+) create mode 100644 specs/changes/public-preview-release-gate/change.json create mode 100644 specs/changes/public-preview-release-gate/delta.md create mode 100644 specs/changes/public-preview-release-gate/design.md create mode 100644 specs/changes/public-preview-release-gate/evidence.md create mode 100644 specs/changes/public-preview-release-gate/proposal.md create mode 100644 specs/changes/public-preview-release-gate/tasks.md diff --git a/specs/changes/public-preview-release-gate/change.json b/specs/changes/public-preview-release-gate/change.json new file mode 100644 index 0000000..a13aa86 --- /dev/null +++ b/specs/changes/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": "draft", + "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": false, + "approval": { + "by": "", + "at": "" + }, + "withdrawal": { + "by": "", + "at": "", + "reason": "" + }, + "createdAt": "2026-08-16", + "archivedAt": null +} diff --git a/specs/changes/public-preview-release-gate/delta.md b/specs/changes/public-preview-release-gate/delta.md new file mode 100644 index 0000000..47fe46a --- /dev/null +++ b/specs/changes/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/changes/public-preview-release-gate/design.md b/specs/changes/public-preview-release-gate/design.md new file mode 100644 index 0000000..89dc861 --- /dev/null +++ b/specs/changes/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/changes/public-preview-release-gate/evidence.md b/specs/changes/public-preview-release-gate/evidence.md new file mode 100644 index 0000000..42863ac --- /dev/null +++ b/specs/changes/public-preview-release-gate/evidence.md @@ -0,0 +1,40 @@ +# Evidence: Decouple public preview from certification evidence + +Implementation evidence is pending proposal approval. Final evidence will retain missing and +excluded results rather than treating public preview as certification. + +## Requirement: OUT-02 + +- Validation: pending approved public-preview decision. +- Verification: pending transition tests and preview release record. +- Result: pending. + +## Requirement: OUT-03 + +- Validation: pending approved public-preview decision. +- Verification: pending transition tests and preview release record. +- Result: pending. + +## Requirement: OUT-04 + +- Validation: pending approved public-preview decision. +- Verification: pending transition tests and claim-consistency checks. +- Result: pending. + +## Requirement: COMPAT-01 + +- Validation: pending approved public-preview decision. +- Verification: pending support-policy and preview-record checks. +- Result: pending. + +## Requirement: DIST-03 + +- Validation: pending approved public-preview decision. +- Verification: pending property model and exact registry evidence. +- Result: pending. + +## Requirement: QUAL-08 + +- Validation: pending approved public-preview decision. +- Verification: pending failure-visibility and documentation-consistency checks. +- Result: pending. diff --git a/specs/changes/public-preview-release-gate/proposal.md b/specs/changes/public-preview-release-gate/proposal.md new file mode 100644 index 0000000..1717c6b --- /dev/null +++ b/specs/changes/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/changes/public-preview-release-gate/tasks.md b/specs/changes/public-preview-release-gate/tasks.md new file mode 100644 index 0000000..08452b2 --- /dev/null +++ b/specs/changes/public-preview-release-gate/tasks.md @@ -0,0 +1,10 @@ +# Tasks: Decouple public preview from certification evidence + +- [ ] Confirm proposal validation and human approval. +- [ ] Add the preview/certification transition property model and deterministic claim-consistency tests. +- [ ] Update product spec, traceability, roadmap Phase 0, Goal 1 runbook, support/release policy, README, and release template. +- [ ] Add a complete public-preview candidate evidence record with every hard gate and missing certification input visible. +- [ ] Verify or configure the exact npm trusted publisher after package-owner authentication. +- [ ] Publish only after the authorized preview candidate passes; retain registry-byte, signature, and provenance verification. +- [ ] Record validation and verification evidence and update affected `specs/current/` files. +- [ ] Run repository validation and archive the packet. From 2c603e348bc9b2f8e3b188336db005b33866a664 Mon Sep 17 00:00:00 2001 From: bitgorust Date: Sun, 16 Aug 2026 19:16:57 +0200 Subject: [PATCH 15/18] feat: add public preview release gate --- .github/workflows/publish.yml | 12 ++ README.md | 7 +- docs/engineering-principles.md | 5 + .../journeys/phase-0-baseline-status.md | 9 +- .../releases/2026-08-16-v0.14.4-preview.md | 183 ++++++++++++++++++ docs/goal-runbook.md | 52 +++-- docs/product-spec.md | 56 ++++-- docs/release-evidence-template.md | 37 +++- docs/requirements-traceability.md | 7 +- docs/roadmap.md | 40 +++- docs/support-policy.md | 27 ++- package-lock.json | 4 +- package.json | 2 +- scripts/governance-policy.ts | 8 + scripts/release-integrity.ts | 98 +++++++++- .../journey-corpus-and-baseline/evidence.md | 6 +- .../journey-corpus-and-baseline/tasks.md | 3 +- .../evidence.md | 4 +- .../platform-security-privacy-policy/tasks.md | 3 +- .../public-preview-release-gate/change.json | 6 +- .../public-preview-release-gate/evidence.md | 74 +++++-- .../public-preview-release-gate/tasks.md | 8 +- specs/current/governance-policy.spec.md | 12 +- specs/current/journey-evidence.spec.md | 7 +- test/governance-policy.test.ts | 2 + test/release-integrity.test.ts | 75 +++++++ 26 files changed, 640 insertions(+), 107 deletions(-) create mode 100644 docs/evidence/releases/2026-08-16-v0.14.4-preview.md diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 873db80..786de50 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,6 +23,8 @@ jobs: - run: npm run build - run: npm test - run: npm run check + - 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 @@ -63,3 +65,13 @@ jobs: 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/README.md b/README.md index 5673210..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) @@ -183,7 +188,7 @@ browser-verified screenshots in [`docs/evidence/patterns/`](docs/evidence/patter ## Governance and support -Current policy is explicit about incomplete evidence: +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; diff --git a/docs/engineering-principles.md b/docs/engineering-principles.md index 86a0519..537f01f 100644 --- a/docs/engineering-principles.md +++ b/docs/engineering-principles.md @@ -211,6 +211,11 @@ collectively exhausted by [`docs/product-spec.md`](product-spec.md), not repeate 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/journeys/phase-0-baseline-status.md b/docs/evidence/journeys/phase-0-baseline-status.md index 078ef9a..90df60d 100644 --- a/docs/evidence/journeys/phase-0-baseline-status.md +++ b/docs/evidence/journeys/phase-0-baseline-status.md @@ -5,10 +5,13 @@ - 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`: missing; no eligible first-time-user clean-machine result -- `OUT-03`: missing; the minimum ten representative primary-user results do not exist +- `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. -Phase 0 remains incomplete until the approved protocol is executed with real consented people. +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..5b935b5 --- /dev/null +++ b/docs/evidence/releases/2026-08-16-v0.14.4-preview.md @@ -0,0 +1,183 @@ +# Release evidence: `v0.14.4` + +Status: Draft preview candidate + +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` and + `supply-chain-vulnerability-remediation`; `public-preview-release-gate` and the two Phase 0 + evidence packets remain active until this release is verified. +- 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` | Pending final preview transition | This record and exact tag workflow | `bitgorust` | SemVer/notes complete; provider/post-publish gates pending | +| `DIST-04`–`DIST-05` | Pass for local candidate; tag verification pending | local candidate plus CI evidence artifact and governance reports | Release maintainer | Zero audit findings and exact dispositions; rerun on tag bytes required | +| `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 | Quality maintainer | Final tag rerun pending | +| `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` | Ubuntu 24.04, Node 24.19.0 | Pass, 130/130 local candidate; exact tag rerun pending | command output and tag workflow | +| `npm run build` | Ubuntu 24.04, Node 24.19.0 | Pass local candidate; exact tag rerun pending | command output and tag workflow | +| `npm run check` | Ubuntu 24.04, Node 24.19.0 | Pass, 35/35 local candidate; exact tag rerun pending | command output and tag workflow | +| `npm pack --dry-run` | Ubuntu 24.04, npm 11.17.0 | Pass, 43 files; exact tag rerun pending | command output and tag workflow | +| 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 is accepted; any exact tag test failure aborts before publication. + +## Public-preview transition + +| Pre-publish hard gate | Pass / Fail | Exact evidence | +|---|---|---| +| tests | Pass local candidate; tag rerun pending | 130/130 under Node 24.19.0 | +| build | Pass local candidate; tag rerun pending | TypeScript build exits 0 | +| structural checks | Pass local candidate; tag rerun pending | 35/35 registered invariants | +| exact package contents/coordinate | Pass local candidate; tag rerun pending | `opencode-artifacts-0.14.4.tgz`, 50,698 bytes, 43 files, SHA-256 `7a529bb0cb5cc2460be7df4183315186bf7034dc1284f496448584f2e020de1e` | +| final secret scan and CSP | Pass local candidate; tag rerun pending | governance, guard, renderer-security tests and structural check | +| vulnerability audit | Pass local candidate; tag rerun pending | zero findings at every severity across 217 dependencies | +| license disposition | Pass local candidate; tag rerun pending | 217 entries; all missing/compound cases hash-disposed | +| redistribution inventory | Pass local candidate; tag rerun pending | 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 | Pending | authenticated package-owner verification | + +| Post-publish hard gate | Pass / Fail | Exact evidence | +|---|---|---| +| registry integrity equals packed bytes | Pending | tag workflow registry output | +| registry package signature | Pending | tag workflow registry output | +| registry provenance for exact tag/commit/workflow | Pending | tag workflow registry output | + +Transition result: **development** until every pre-publish row passes; then +`preview-candidate`. It becomes `public-preview` only after all post-publish rows pass. + +## 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; + exact tag rerun required for any release-specific statement. +- 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; exact tag workflow reruns them. +- Final-content secret scan: required pre-publish; exact run pending. +- 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 pending. +- 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 registry | public package | trusted publish and exact provenance | Pending | tag workflow | +| GitHub Pages / Cloudflare / authenticated / connector | not claimed | certification-only providers | Incomplete | not promoted by preview | + +## Supply-chain outputs + +- Packed local candidate: `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 pending exact tag. +- CycloneDX JSON: pending exact tag. +- Trusted publisher and published provenance: pending authenticated verification and exact tag. +- Registry signature verification: pending exact tag. +- Dependency vulnerability and license disposition: current candidate passes; tag rerun is + authoritative. +- 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: npm trusted-publisher owner verification and exact tag pre/post-publish gates. +- 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: verify trusted publisher; commit and pass exact branch CI; tag only the coordinated + package version; watch publish; verify registry bytes/signature/provenance; deprecate and + correct forward if post-publish verification fails. +- 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 install diagnostic; + no SLA. +- Incident/support contact: private GitHub advisory path; public preview has no support SLA. + +Final decision: **Pending 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/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/release-evidence-template.md b/docs/release-evidence-template.md index 64f62a7..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: @@ -123,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/requirements-traceability.md b/docs/requirements-traceability.md index db565af..a464df1 100644 --- a/docs/requirements-traceability.md +++ b/docs/requirements-traceability.md @@ -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` | diff --git a/docs/roadmap.md b/docs/roadmap.md index cdb3a37..790917d 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -67,15 +67,18 @@ 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. @@ -90,15 +93,22 @@ users, support envelope, data handling, or acceptance evidence cannot be named. 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. +- [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. ## Phase 1 — Durable artifact identity and transactions @@ -351,12 +361,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; preview provider verification pending | ## Risk register @@ -376,7 +386,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 +428,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/support-policy.md b/docs/support-policy.md index 8551944..954f488 100644 --- a/docs/support-policy.md +++ b/docs/support-policy.md @@ -14,6 +14,12 @@ removal. It did not exercise Firefox, the latest two stable browser generations, 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 | @@ -48,12 +54,21 @@ notice would extend an active exploit; release notes must record the security ex without disclosing usable exploit detail. End-of-life begins when a newer minor becomes the supported minor. Unsupported versions receive no fix promise. -## Release gate - -Every release uses [the release evidence template](release-evidence-template.md) and names one -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 the claim; “not applicable” requires a reason. +## 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/), diff --git a/package-lock.json b/package-lock.json index f5c5050..c4cd870 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "opencode-artifacts", - "version": "0.14.3", + "version": "0.14.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opencode-artifacts", - "version": "0.14.3", + "version": "0.14.4", "license": "MIT", "dependencies": { "echarts": "^6.1.0", diff --git a/package.json b/package.json index 033bff2..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", diff --git a/scripts/governance-policy.ts b/scripts/governance-policy.ts index 999d4d2..71d46f2 100644 --- a/scripts/governance-policy.ts +++ b/scripts/governance-policy.ts @@ -434,12 +434,20 @@ export function validateGovernanceClaims(inputs: GovernanceClaimInputs): string[ 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"); } diff --git a/scripts/release-integrity.ts b/scripts/release-integrity.ts index c4838ca..d4506f6 100644 --- a/scripts/release-integrity.ts +++ b/scripts/release-integrity.ts @@ -251,6 +251,87 @@ export function releaseCandidateGateFailures(checks: ReleaseCandidateChecks): st .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; @@ -346,6 +427,14 @@ export function verifyPublishedDistribution(pack: unknown, dist: unknown): strin 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; } @@ -403,7 +492,14 @@ async function main(): Promise { else console.log("ok - registry integrity, signature, and provenance match the packed release"); return; } - console.error("Usage: release-integrity.ts licenses | pack-output | candidate-provenance | verify-registry "); + 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; } diff --git a/specs/changes/journey-corpus-and-baseline/evidence.md b/specs/changes/journey-corpus-and-baseline/evidence.md index 2aa0028..2c54903 100644 --- a/specs/changes/journey-corpus-and-baseline/evidence.md +++ b/specs/changes/journey-corpus-and-baseline/evidence.md @@ -6,7 +6,8 @@ - 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: missing, which is not a pass. +- 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) @@ -15,7 +16,8 @@ - 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: missing, which keeps the Phase 0 gate failed. +- 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) diff --git a/specs/changes/journey-corpus-and-baseline/tasks.md b/specs/changes/journey-corpus-and-baseline/tasks.md index 647c56c..fab6a96 100644 --- a/specs/changes/journey-corpus-and-baseline/tasks.md +++ b/specs/changes/journey-corpus-and-baseline/tasks.md @@ -4,7 +4,8 @@ - [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. -- [ ] Run OUT-02 and OUT-03 with consented representative participants and retain real results. +- [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. - [ ] Run repository validation and archive the packet. diff --git a/specs/changes/platform-security-privacy-policy/evidence.md b/specs/changes/platform-security-privacy-policy/evidence.md index 8b71e0b..a3c3e42 100644 --- a/specs/changes/platform-security-privacy-policy/evidence.md +++ b/specs/changes/platform-security-privacy-policy/evidence.md @@ -1,7 +1,9 @@ # Evidence: Publish platform, security, privacy, and release policy This record separates policy publication from current operational evidence. Failed and -unverified results keep the packet open and block the Phase 0 gate. +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 diff --git a/specs/changes/platform-security-privacy-policy/tasks.md b/specs/changes/platform-security-privacy-policy/tasks.md index 9ce17bc..3fa0058 100644 --- a/specs/changes/platform-security-privacy-policy/tasks.md +++ b/specs/changes/platform-security-privacy-policy/tasks.md @@ -4,7 +4,8 @@ - [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. -- [ ] Run every claimed host/browser cell and retain exact real results; leave unavailable cells unverified. +- [x] Retain exact available host/browser observations and leave every unavailable target cell + unverified; defer support promotion to certification rather than blocking public preview. - [ ] 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. diff --git a/specs/changes/public-preview-release-gate/change.json b/specs/changes/public-preview-release-gate/change.json index a13aa86..2f27844 100644 --- a/specs/changes/public-preview-release-gate/change.json +++ b/specs/changes/public-preview-release-gate/change.json @@ -3,7 +3,7 @@ "id": "public-preview-release-gate", "title": "Decouple public preview from certification evidence", "lane": "high-risk", - "status": "draft", + "status": "implementing", "affectedRequirements": [ "OUT-02", "OUT-03", @@ -18,8 +18,8 @@ ], "currentSpecsUpdated": false, "approval": { - "by": "", - "at": "" + "by": "bitgorust", + "at": "2026-08-16T16:50:30Z" }, "withdrawal": { "by": "", diff --git a/specs/changes/public-preview-release-gate/evidence.md b/specs/changes/public-preview-release-gate/evidence.md index 42863ac..0f01212 100644 --- a/specs/changes/public-preview-release-gate/evidence.md +++ b/specs/changes/public-preview-release-gate/evidence.md @@ -1,40 +1,74 @@ # Evidence: Decouple public preview from certification evidence -Implementation evidence is pending proposal approval. Final evidence will retain missing and -excluded results rather than treating public preview as certification. +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. The exact npm provider and post-publication results +remain pending and therefore keep the transition in `development`. ## Requirement: OUT-02 -- Validation: pending approved public-preview decision. -- Verification: pending transition tests and preview release record. -- Result: pending. +- 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: pending approved public-preview decision. -- Verification: pending transition tests and preview release record. -- Result: pending. +- 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: pending approved public-preview decision. -- Verification: pending transition tests and claim-consistency checks. -- Result: pending. +- 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; exact release transition remains pending. +- Evidence: [@test](test/release-integrity.test.ts), + [@manual](docs/requirements-traceability.md) ## Requirement: COMPAT-01 -- Validation: pending approved public-preview decision. -- Verification: pending support-policy and preview-record checks. -- Result: pending. +- 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: pending approved public-preview decision. -- Verification: pending property model and exact registry evidence. -- Result: pending. +- 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. Exact npm trusted-publisher, registry-byte, signature, and provenance evidence is + still pending. +- Result: partial; the candidate remains `development` and publication is blocked. +- Evidence: [@test](test/release-integrity.test.ts), + [@manual](docs/evidence/releases/2026-08-16-v0.14.4-preview.md) ## Requirement: QUAL-08 -- Validation: pending approved public-preview decision. -- Verification: pending failure-visibility and documentation-consistency checks. -- Result: pending. +- 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 candidate documentation; final release status remains pending. +- Evidence: [@test](test/governance-policy.test.ts), + [@manual](docs/evidence/releases/2026-08-16-v0.14.4-preview.md) diff --git a/specs/changes/public-preview-release-gate/tasks.md b/specs/changes/public-preview-release-gate/tasks.md index 08452b2..4bcd387 100644 --- a/specs/changes/public-preview-release-gate/tasks.md +++ b/specs/changes/public-preview-release-gate/tasks.md @@ -1,9 +1,9 @@ # Tasks: Decouple public preview from certification evidence -- [ ] Confirm proposal validation and human approval. -- [ ] Add the preview/certification transition property model and deterministic claim-consistency tests. -- [ ] Update product spec, traceability, roadmap Phase 0, Goal 1 runbook, support/release policy, README, and release template. -- [ ] Add a complete public-preview candidate evidence record with every hard gate and missing certification input visible. +- [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. - [ ] Verify or configure the exact npm trusted publisher after package-owner authentication. - [ ] Publish only after the authorized preview candidate passes; retain registry-byte, signature, and provenance verification. - [ ] Record validation and verification evidence and update affected `specs/current/` files. diff --git a/specs/current/governance-policy.spec.md b/specs/current/governance-policy.spec.md index 6541839..c5eb8d6 100644 --- a/specs/current/governance-policy.spec.md +++ b/specs/current/governance-policy.spec.md @@ -16,7 +16,8 @@ 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. + 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 @@ -41,6 +42,9 @@ 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 claim is permitted. The tag workflow fails closed on - prepublish evidence, publishes the coordinated tarball, and verifies registry integrity/ - signature/provenance afterward. + 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. Preview history supplies no + certification waiver. diff --git a/specs/current/journey-evidence.spec.md b/specs/current/journey-evidence.spec.md index 3b99eac..690524c 100644 --- a/specs/current/journey-evidence.spec.md +++ b/specs/current/journey-evidence.spec.md @@ -31,5 +31,8 @@ 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 missing and the Phase 0 gate remains - incomplete until the approved real study is run. + 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/test/governance-policy.test.ts b/test/governance-policy.test.ts index 8be6108..ef6bf0e 100644 --- a/test/governance-policy.test.ts +++ b/test/governance-policy.test.ts @@ -56,6 +56,8 @@ test("inflated README provenance and mismatched Node claims fail consistency", ( 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", () => { diff --git a/test/release-integrity.test.ts b/test/release-integrity.test.ts index d7d7e2c..1c154a5 100644 --- a/test/release-integrity.test.ts +++ b/test/release-integrity.test.ts @@ -5,9 +5,14 @@ import { 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", () => { @@ -104,6 +109,76 @@ test("release candidate property model blocks every failed constituent gate", () } }); +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( From a5ee65a588bc659d232431f55f20f404eb20e6d4 Mon Sep 17 00:00:00 2001 From: bitgorust Date: Sun, 16 Aug 2026 19:24:04 +0200 Subject: [PATCH 16/18] docs: verify npm trusted publisher --- .../governance/provider-status-2026-08-16.md | 11 +++++----- .../governance/supply-chain-2026-08-16.md | 6 +++-- .../releases/2026-08-16-v0.14.4-preview.md | 22 ++++++++++--------- docs/governance-policy.json | 4 ++-- docs/release.md | 5 +++-- docs/roadmap.md | 2 +- docs/support-policy.md | 14 +++++------- .../evidence.md | 11 ++++++---- .../platform-security-privacy-policy/tasks.md | 2 +- .../public-preview-release-gate/evidence.md | 13 ++++++----- .../public-preview-release-gate/tasks.md | 2 +- 11 files changed, 51 insertions(+), 41 deletions(-) diff --git a/docs/evidence/governance/provider-status-2026-08-16.md b/docs/evidence/governance/provider-status-2026-08-16.md index bddc100..be4e8c3 100644 --- a/docs/evidence/governance/provider-status-2026-08-16.md +++ b/docs/evidence/governance/provider-status-2026-08-16.md @@ -6,9 +6,10 @@ Repository: `bitgorust/opencode-artifacts` |---|---|---| | 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 / future provenance | Provider configuration is not publicly verified and no future candidate is published | Unverified; prior provenance and workflow configuration cannot pass a future release | +| 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` candidate publisher coordinate; registry provenance still requires the real tag workflow | +| npm registry provenance for 0.14.4 | No 0.14.4 registry bytes exist yet | Pending; the candidate may advance to `preview-candidate`, but not `public-preview`, until registry integrity, signature, and provenance pass | -The GitHub setting was changed only after explicit authorization on 2026-08-16 and was then -read back from the provider. npm trusted-publisher configuration was not changed in this -observation; it still requires an authenticated package-owner session and follow-up publish -verification. +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 passes the pre-publish gate; it does not predict or replace the required +post-publish registry verification. diff --git a/docs/evidence/governance/supply-chain-2026-08-16.md b/docs/evidence/governance/supply-chain-2026-08-16.md index de9601f..861edc3 100644 --- a/docs/evidence/governance/supply-chain-2026-08-16.md +++ b/docs/evidence/governance/supply-chain-2026-08-16.md @@ -67,8 +67,10 @@ its [verification guide](https://docs.npmjs.com/viewing-package-provenance/). 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 and provenance for this unreleased candidate remain -unverified. No registry setting or package was changed. +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 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 index 5b935b5..04559d1 100644 --- a/docs/evidence/releases/2026-08-16-v0.14.4-preview.md +++ b/docs/evidence/releases/2026-08-16-v0.14.4-preview.md @@ -45,7 +45,7 @@ Release owner: `bitgorust` | `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` | Pending final preview transition | This record and exact tag workflow | `bitgorust` | SemVer/notes complete; provider/post-publish gates pending | +| `DIST-03` | Pass for preview candidate; pending final preview transition | This record and exact tag workflow | `bitgorust` | SemVer/notes and trusted publisher complete; post-publish gates pending | | `DIST-04`–`DIST-05` | Pass for local candidate; tag verification pending | local candidate plus CI evidence artifact and governance reports | Release maintainer | Zero audit findings and exact dispositions; rerun on tag bytes required | | `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 | Quality maintainer | Final tag rerun pending | @@ -89,7 +89,7 @@ quarantined test is accepted; any exact tag test failure aborts before publicati | license disposition | Pass local candidate; tag rerun pending | 217 entries; all missing/compound cases hash-disposed | | redistribution inventory | Pass local candidate; tag rerun pending | 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 | Pending | authenticated package-owner verification | +| exact npm trusted-publisher binding | Pass | authenticated `npm trust list`: `bitgorust/opencode-artifacts`, `publish.yml`, `createPackage` | | Post-publish hard gate | Pass / Fail | Exact evidence | |---|---|---| @@ -97,8 +97,8 @@ quarantined test is accepted; any exact tag test failure aborts before publicati | registry package signature | Pending | tag workflow registry output | | registry provenance for exact tag/commit/workflow | Pending | tag workflow registry output | -Transition result: **development** until every pre-publish row passes; then -`preview-candidate`. It becomes `public-preview` only after all post-publish rows pass. +Transition result: **preview-candidate**. It becomes `public-preview` only after all +post-publish rows pass. ## Browser, accessibility, and page quality @@ -140,7 +140,8 @@ and npm deprecation because published name/version bytes cannot be recalled reli | 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 registry | public package | trusted publish and exact provenance | Pending | tag workflow | +| 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 | Pending | tag workflow | | GitHub Pages / Cloudflare / authenticated / connector | not claimed | certification-only providers | Incomplete | not promoted by preview | ## Supply-chain outputs @@ -151,7 +152,8 @@ and npm deprecation because published name/version bytes cannot be recalled reli `sha512-EFycALkIET5z/7rkkvGw3G+xt3+WaOabPwBLZZg1nZ3sxuapYuXxdD7rnyoPbghKnEt2GvZ1XtgtmW6VbUkjwQ==`; tag/commit/workflow/registry cross-check pending exact tag. - CycloneDX JSON: pending exact tag. -- Trusted publisher and published provenance: pending authenticated verification and exact tag. +- Trusted publisher: pass by authenticated exact provider readback; published provenance: + pending exact tag. - Registry signature verification: pending exact tag. - Dependency vulnerability and license disposition: current candidate passes; tag rerun is authoritative. @@ -162,12 +164,12 @@ and npm deprecation because published name/version bytes cannot be recalled reli ## Decision, rollout, and support -- Open blockers: npm trusted-publisher owner verification and exact tag pre/post-publish gates. +- Open blockers: exact tag workflow and post-publish registry gates. - 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: verify trusted publisher; commit and pass exact branch CI; tag only the coordinated - package version; watch publish; verify registry bytes/signature/provenance; deprecate and - correct forward if post-publish verification fails. +- Rollout: commit the verified provider evidence and pass exact branch CI; tag only the + coordinated package version; watch publish; verify registry bytes/signature/provenance; + deprecate and correct forward if post-publish verification fails. - 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. diff --git a/docs/governance-policy.json b/docs/governance-policy.json index ea8fd55..da2841b 100644 --- a/docs/governance-policy.json +++ b/docs/governance-policy.json @@ -18,10 +18,10 @@ }, { "id": "npm-trusted-publishing", - "status": "unverified", + "status": "pass", "checkedAt": "2026-08-16", "evidence": "docs/evidence/governance/provider-status-2026-08-16.md", - "claim": "Published 0.14.3 has registry provenance bound to its GitHub Actions tag/commit; trusted-publisher configuration and provenance for this unreleased candidate remain unverified." + "claim": "The exact GitHub repository/workflow trusted publisher is authenticated and provider-verified for createPackage; 0.14.4 registry integrity, signature, and provenance remain pending until publication." } ], "supportCells": [ 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/roadmap.md b/docs/roadmap.md index 790917d..a541385 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -60,7 +60,7 @@ Evidence from the repository and clean local host probes: | 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 | Partial | privacy inventory/policy exists; operations runbooks, SLO evidence, performance harnesses, and cost models do not | -| Supply-chain release evidence | Partial | audit, hash-bound dependency licenses, and current redistribution inventory pass; CI retains exact candidate SBOM/provenance/pack evidence; future registry provenance and trusted-publisher verification remain open | +| Supply-chain release evidence | Partial | audit, hash-bound dependency licenses, redistribution inventory, and exact trusted-publisher readback pass; CI retains candidate SBOM/provenance/pack evidence; exact 0.14.4 registry integrity/signature/provenance remain open until publication | “Shipped” means the current behavior exists; it does not waive any stronger acceptance rule in the product spec. diff --git a/docs/support-policy.md b/docs/support-policy.md index 954f488..dcefeb3 100644 --- a/docs/support-policy.md +++ b/docs/support-policy.md @@ -85,17 +85,15 @@ requires all of the following for the exact packed bytes: - 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 -provenance for the then-current published release and binds it to its GitHub Actions workflow, -tag, and source commit. Registry trusted-publisher configuration is not -publicly verified, and that prior attestation cannot cover future candidate bytes. A release -still needs its own post-publish verification. Generated SBOM/provenance describes composition/ -origin; it does not prove safety. +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. No tag was -created or release attempted by this policy change. +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. diff --git a/specs/changes/platform-security-privacy-policy/evidence.md b/specs/changes/platform-security-privacy-policy/evidence.md index a3c3e42..18f136c 100644 --- a/specs/changes/platform-security-privacy-policy/evidence.md +++ b/specs/changes/platform-security-privacy-policy/evidence.md @@ -111,10 +111,13 @@ target-matrix gaps do not block unsupported preview distribution. - 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. Published 0.14.3 - provenance remains verified only for its own tag/commit, while trusted-publisher - configuration and a future registry release remain unverified. -- Result: fail for production readiness. + 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 + 0.14.4 registry integrity, signature, and provenance remain pending publication. +- Result: pass for the public-preview pre-publish supply-chain prerequisite; incomplete for + 0.14.4 post-publish evidence and production readiness. - 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), diff --git a/specs/changes/platform-security-privacy-policy/tasks.md b/specs/changes/platform-security-privacy-policy/tasks.md index 3fa0058..b6d4695 100644 --- a/specs/changes/platform-security-privacy-policy/tasks.md +++ b/specs/changes/platform-security-privacy-policy/tasks.md @@ -6,7 +6,7 @@ - [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. -- [ ] Obtain explicit authority before enabling GitHub private vulnerability reporting or changing npm trusted publishing. +- [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. diff --git a/specs/changes/public-preview-release-gate/evidence.md b/specs/changes/public-preview-release-gate/evidence.md index 0f01212..eb0e157 100644 --- a/specs/changes/public-preview-release-gate/evidence.md +++ b/specs/changes/public-preview-release-gate/evidence.md @@ -2,8 +2,9 @@ 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. The exact npm provider and post-publication results -remain pending and therefore keep the transition in `development`. +treating public preview as certification. Authenticated provider readback verifies the exact +npm trusted publisher; post-publication results remain pending, so the transition is +`preview-candidate` rather than `public-preview`. ## Requirement: OUT-02 @@ -56,9 +57,11 @@ remain pending and therefore keep the transition in `development`. - 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. Exact npm trusted-publisher, registry-byte, signature, and provenance evidence is - still pending. -- Result: partial; the candidate remains `development` and publication is blocked. + candidate. Authenticated npm readback verifies repository `bitgorust/opencode-artifacts`, + workflow `publish.yml`, and permission `createPackage`; registry-byte, signature, and + provenance evidence is still pending. +- Result: pass for `preview-candidate`; `public-preview` remains blocked pending the exact tag + workflow and registry verification. - Evidence: [@test](test/release-integrity.test.ts), [@manual](docs/evidence/releases/2026-08-16-v0.14.4-preview.md) diff --git a/specs/changes/public-preview-release-gate/tasks.md b/specs/changes/public-preview-release-gate/tasks.md index 4bcd387..a39379d 100644 --- a/specs/changes/public-preview-release-gate/tasks.md +++ b/specs/changes/public-preview-release-gate/tasks.md @@ -4,7 +4,7 @@ - [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. -- [ ] Verify or configure the exact npm trusted publisher after package-owner authentication. +- [x] Verify or configure the exact npm trusted publisher after package-owner authentication. - [ ] Publish only after the authorized preview candidate passes; retain registry-byte, signature, and provenance verification. - [ ] Record validation and verification evidence and update affected `specs/current/` files. - [ ] Run repository validation and archive the packet. From b97a9b27ef5fe569dd997a380dee5fbed757e4e3 Mon Sep 17 00:00:00 2001 From: bitgorust Date: Sun, 16 Aug 2026 19:31:18 +0200 Subject: [PATCH 17/18] fix: accept npm view registry arrays --- scripts/release-integrity.ts | 13 +++++++++---- test/release-integrity.test.ts | 8 ++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/scripts/release-integrity.ts b/scripts/release-integrity.ts index d4506f6..f084dc0 100644 --- a/scripts/release-integrity.ts +++ b/scripts/release-integrity.ts @@ -410,18 +410,23 @@ export function candidateProvenance( export function verifyPublishedDistribution(pack: unknown, dist: unknown): string[] { const errors: string[] = []; - if (!isRecord(pack) || !isRecord(dist)) return ["pack coordinate and registry dist must be objects"]; + 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" || dist[field] !== pack[field]) { + if (typeof pack[field] !== "string" || registryDist[field] !== pack[field]) { errors.push(`registry ${field} does not match the packed bytes`); } } - const attestations = dist["attestations"]; + 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(dist["signatures"]) || dist["signatures"].length === 0) { + if (!Array.isArray(registryDist["signatures"]) || registryDist["signatures"].length === 0) { errors.push("registry package signature is missing"); } return errors; diff --git a/test/release-integrity.test.ts b/test/release-integrity.test.ts index 1c154a5..64147ea 100644 --- a/test/release-integrity.test.ts +++ b/test/release-integrity.test.ts @@ -244,6 +244,14 @@ test("registry verification requires matching bytes, signature, and provenance", }), [], ); + 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: [] }), [ From 87c8c9693ae119f1bb449aa24e03dd0dbb6baab0 Mon Sep 17 00:00:00 2001 From: bitgorust Date: Sun, 16 Aug 2026 19:35:47 +0200 Subject: [PATCH 18/18] docs: record v0.14.4 public preview --- .../governance/provider-status-2026-08-16.md | 9 +- .../releases/2026-08-16-v0.14.4-preview.md | 97 ++++++++++--------- docs/governance-policy.json | 2 +- docs/roadmap.md | 7 +- .../change.json | 4 +- .../delta.md | 0 .../evidence.md | 0 .../proposal.md | 0 .../tasks.md | 2 +- .../change.json | 4 +- .../delta.md | 0 .../design.md | 0 .../evidence.md | 8 +- .../proposal.md | 0 .../tasks.md | 2 +- .../change.json | 6 +- .../delta.md | 0 .../design.md | 0 .../evidence.md | 19 ++-- .../proposal.md | 0 .../tasks.md | 6 +- specs/current/governance-policy.spec.md | 9 +- 22 files changed, 96 insertions(+), 79 deletions(-) rename specs/{changes/journey-corpus-and-baseline => archive/2026-08-16-journey-corpus-and-baseline}/change.json (90%) rename specs/{changes/journey-corpus-and-baseline => archive/2026-08-16-journey-corpus-and-baseline}/delta.md (100%) rename specs/{changes/journey-corpus-and-baseline => archive/2026-08-16-journey-corpus-and-baseline}/evidence.md (100%) rename specs/{changes/journey-corpus-and-baseline => archive/2026-08-16-journey-corpus-and-baseline}/proposal.md (100%) rename specs/{changes/journey-corpus-and-baseline => archive/2026-08-16-journey-corpus-and-baseline}/tasks.md (92%) rename specs/{changes/platform-security-privacy-policy => archive/2026-08-16-platform-security-privacy-policy}/change.json (92%) rename specs/{changes/platform-security-privacy-policy => archive/2026-08-16-platform-security-privacy-policy}/delta.md (100%) rename specs/{changes/platform-security-privacy-policy => archive/2026-08-16-platform-security-privacy-policy}/design.md (100%) rename specs/{changes/platform-security-privacy-policy => archive/2026-08-16-platform-security-privacy-policy}/evidence.md (95%) rename specs/{changes/platform-security-privacy-policy => archive/2026-08-16-platform-security-privacy-policy}/proposal.md (100%) rename specs/{changes/platform-security-privacy-policy => archive/2026-08-16-platform-security-privacy-policy}/tasks.md (94%) rename specs/{changes/public-preview-release-gate => archive/2026-08-16-public-preview-release-gate}/change.json (87%) rename specs/{changes/public-preview-release-gate => archive/2026-08-16-public-preview-release-gate}/delta.md (100%) rename specs/{changes/public-preview-release-gate => archive/2026-08-16-public-preview-release-gate}/design.md (100%) rename specs/{changes/public-preview-release-gate => archive/2026-08-16-public-preview-release-gate}/evidence.md (81%) rename specs/{changes/public-preview-release-gate => archive/2026-08-16-public-preview-release-gate}/proposal.md (100%) rename specs/{changes/public-preview-release-gate => archive/2026-08-16-public-preview-release-gate}/tasks.md (78%) diff --git a/docs/evidence/governance/provider-status-2026-08-16.md b/docs/evidence/governance/provider-status-2026-08-16.md index be4e8c3..88f322b 100644 --- a/docs/evidence/governance/provider-status-2026-08-16.md +++ b/docs/evidence/governance/provider-status-2026-08-16.md @@ -6,10 +6,11 @@ Repository: `bitgorust/opencode-artifacts` |---|---|---| | 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` candidate publisher coordinate; registry provenance still requires the real tag workflow | -| npm registry provenance for 0.14.4 | No 0.14.4 registry bytes exist yet | Pending; the candidate may advance to `preview-candidate`, but not `public-preview`, until registry integrity, signature, and provenance pass | +| 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 passes the pre-publish gate; it does not predict or replace the required -post-publish registry verification. +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/releases/2026-08-16-v0.14.4-preview.md b/docs/evidence/releases/2026-08-16-v0.14.4-preview.md index 04559d1..3f5c91a 100644 --- a/docs/evidence/releases/2026-08-16-v0.14.4-preview.md +++ b/docs/evidence/releases/2026-08-16-v0.14.4-preview.md @@ -1,6 +1,6 @@ # Release evidence: `v0.14.4` -Status: Draft preview candidate +Status: Passed public preview Decision date: 2026-08-16 @@ -17,9 +17,9 @@ Release owner: `bitgorust` scanning. - Preview/support/certification label shown to users: public preview; unsupported; uncertified; zero supported platform/browser cells. -- Archived change packets included: `contract-link-integrity` and - `supply-chain-vulnerability-remediation`; `public-preview-release-gate` and the two Phase 0 - evidence packets remain active until this release is verified. +- 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 @@ -45,10 +45,10 @@ Release owner: `bitgorust` | `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 preview candidate; pending final preview transition | This record and exact tag workflow | `bitgorust` | SemVer/notes and trusted publisher complete; post-publish gates pending | -| `DIST-04`–`DIST-05` | Pass for local candidate; tag verification pending | local candidate plus CI evidence artifact and governance reports | Release maintainer | Zero audit findings and exact dispositions; rerun on tag bytes required | +| `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 | Quality maintainer | Final tag rerun pending | +| `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 | @@ -66,39 +66,46 @@ Release owner: `bitgorust` | Command / job | Environment | Result | Output/evidence path | |---|---|---|---| -| `npm test` | Ubuntu 24.04, Node 24.19.0 | Pass, 130/130 local candidate; exact tag rerun pending | command output and tag workflow | -| `npm run build` | Ubuntu 24.04, Node 24.19.0 | Pass local candidate; exact tag rerun pending | command output and tag workflow | -| `npm run check` | Ubuntu 24.04, Node 24.19.0 | Pass, 35/35 local candidate; exact tag rerun pending | command output and tag workflow | -| `npm pack --dry-run` | Ubuntu 24.04, npm 11.17.0 | Pass, 43 files; exact tag rerun pending | command output and tag workflow | +| `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 is accepted; any exact tag test failure aborts before publication. +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 local candidate; tag rerun pending | 130/130 under Node 24.19.0 | -| build | Pass local candidate; tag rerun pending | TypeScript build exits 0 | -| structural checks | Pass local candidate; tag rerun pending | 35/35 registered invariants | -| exact package contents/coordinate | Pass local candidate; tag rerun pending | `opencode-artifacts-0.14.4.tgz`, 50,698 bytes, 43 files, SHA-256 `7a529bb0cb5cc2460be7df4183315186bf7034dc1284f496448584f2e020de1e` | -| final secret scan and CSP | Pass local candidate; tag rerun pending | governance, guard, renderer-security tests and structural check | -| vulnerability audit | Pass local candidate; tag rerun pending | zero findings at every severity across 217 dependencies | -| license disposition | Pass local candidate; tag rerun pending | 217 entries; all missing/compound cases hash-disposed | -| redistribution inventory | Pass local candidate; tag rerun pending | machine-checked inventory and 43-file package review | +| 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 | Pending | tag workflow registry output | -| registry package signature | Pending | tag workflow registry output | -| registry provenance for exact tag/commit/workflow | Pending | tag workflow registry output | +| 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: **preview-candidate**. It becomes `public-preview` only after all -post-publish rows pass. +Transition result: **public-preview**. ## Browser, accessibility, and page quality @@ -107,7 +114,7 @@ post-publish rows pass. and certified matrix absent. - Accessibility scanner and manual findings: incomplete. - Console, asset, overflow, and offline-network results: prior exact candidate diagnostic only; - exact tag rerun required for any release-specific statement. + 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. @@ -116,12 +123,12 @@ post-publish rows pass. - 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; exact tag workflow reruns them. -- Final-content secret scan: required pre-publish; exact run pending. + 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 pending. + 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 @@ -141,22 +148,22 @@ and npm deprecation because published name/version bytes cannot be recalled reli | 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 | Pending | tag workflow | +| 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 local candidate: `opencode-artifacts-0.14.4.tgz`, 50,698 bytes, SHA-256 +- 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 pending exact tag. -- CycloneDX JSON: pending exact tag. -- Trusted publisher: pass by authenticated exact provider readback; published provenance: - pending exact tag. -- Registry signature verification: pending exact tag. -- Dependency vulnerability and license disposition: current candidate passes; tag rerun is - authoritative. + 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 @@ -164,20 +171,20 @@ and npm deprecation because published name/version bytes cannot be recalled reli ## Decision, rollout, and support -- Open blockers: exact tag workflow and post-publish registry gates. +- 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: commit the verified provider evidence and pass exact branch CI; tag only the - coordinated package version; watch publish; verify registry bytes/signature/provenance; - deprecate and correct forward if post-publish verification fails. +- 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 install diagnostic; - no SLA. +- 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: **Pending public preview** +Final decision: **Public preview** Accountable release owner: `bitgorust` diff --git a/docs/governance-policy.json b/docs/governance-policy.json index da2841b..34da67e 100644 --- a/docs/governance-policy.json +++ b/docs/governance-policy.json @@ -21,7 +21,7 @@ "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 authenticated and provider-verified for createPackage; 0.14.4 registry integrity, signature, and provenance remain pending until publication." + "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": [ diff --git a/docs/roadmap.md b/docs/roadmap.md index a541385..d99aee6 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -60,7 +60,7 @@ Evidence from the repository and clean local host probes: | 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 | Partial | privacy inventory/policy exists; operations runbooks, SLO evidence, performance harnesses, and cost models do not | -| Supply-chain release evidence | Partial | audit, hash-bound dependency licenses, redistribution inventory, and exact trusted-publisher readback pass; CI retains candidate SBOM/provenance/pack evidence; exact 0.14.4 registry integrity/signature/provenance remain open until publication | +| 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. @@ -110,6 +110,9 @@ gate and keeps first-use, comprehension, and target-platform evidence visibly in 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 Goal: make local publishing correct under multiple sessions, processes, crashes, and upgrades. @@ -366,7 +369,7 @@ consequences, owner, and review trigger. “Use the expected shape” is not a d | `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 public preview or any production-readiness claim | Release maintainer | Distribution gate | Selected; preview provider verification pending | +| `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 diff --git a/specs/changes/journey-corpus-and-baseline/change.json b/specs/archive/2026-08-16-journey-corpus-and-baseline/change.json similarity index 90% rename from specs/changes/journey-corpus-and-baseline/change.json rename to specs/archive/2026-08-16-journey-corpus-and-baseline/change.json index 651b328..6275e74 100644 --- a/specs/changes/journey-corpus-and-baseline/change.json +++ b/specs/archive/2026-08-16-journey-corpus-and-baseline/change.json @@ -3,7 +3,7 @@ "id": "journey-corpus-and-baseline", "title": "Establish the journey corpus and baseline study", "lane": "standard", - "status": "implementing", + "status": "archived", "affectedRequirements": [ "OUT-02", "OUT-03", @@ -24,5 +24,5 @@ "reason": "" }, "createdAt": "2026-08-16", - "archivedAt": null + "archivedAt": "2026-08-16" } diff --git a/specs/changes/journey-corpus-and-baseline/delta.md b/specs/archive/2026-08-16-journey-corpus-and-baseline/delta.md similarity index 100% rename from specs/changes/journey-corpus-and-baseline/delta.md rename to specs/archive/2026-08-16-journey-corpus-and-baseline/delta.md diff --git a/specs/changes/journey-corpus-and-baseline/evidence.md b/specs/archive/2026-08-16-journey-corpus-and-baseline/evidence.md similarity index 100% rename from specs/changes/journey-corpus-and-baseline/evidence.md rename to specs/archive/2026-08-16-journey-corpus-and-baseline/evidence.md diff --git a/specs/changes/journey-corpus-and-baseline/proposal.md b/specs/archive/2026-08-16-journey-corpus-and-baseline/proposal.md similarity index 100% rename from specs/changes/journey-corpus-and-baseline/proposal.md rename to specs/archive/2026-08-16-journey-corpus-and-baseline/proposal.md diff --git a/specs/changes/journey-corpus-and-baseline/tasks.md b/specs/archive/2026-08-16-journey-corpus-and-baseline/tasks.md similarity index 92% rename from specs/changes/journey-corpus-and-baseline/tasks.md rename to specs/archive/2026-08-16-journey-corpus-and-baseline/tasks.md index fab6a96..89bc221 100644 --- a/specs/changes/journey-corpus-and-baseline/tasks.md +++ b/specs/archive/2026-08-16-journey-corpus-and-baseline/tasks.md @@ -8,4 +8,4 @@ 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. -- [ ] Run repository validation and archive the packet. +- [x] Run repository validation and archive the packet. diff --git a/specs/changes/platform-security-privacy-policy/change.json b/specs/archive/2026-08-16-platform-security-privacy-policy/change.json similarity index 92% rename from specs/changes/platform-security-privacy-policy/change.json rename to specs/archive/2026-08-16-platform-security-privacy-policy/change.json index d501ad3..fca6391 100644 --- a/specs/changes/platform-security-privacy-policy/change.json +++ b/specs/archive/2026-08-16-platform-security-privacy-policy/change.json @@ -3,7 +3,7 @@ "id": "platform-security-privacy-policy", "title": "Publish platform, security, privacy, and release policy", "lane": "high-risk", - "status": "implementing", + "status": "archived", "affectedRequirements": [ "SEC-01", "SEC-10", @@ -35,5 +35,5 @@ "reason": "" }, "createdAt": "2026-08-16", - "archivedAt": null + "archivedAt": "2026-08-16" } diff --git a/specs/changes/platform-security-privacy-policy/delta.md b/specs/archive/2026-08-16-platform-security-privacy-policy/delta.md similarity index 100% rename from specs/changes/platform-security-privacy-policy/delta.md rename to specs/archive/2026-08-16-platform-security-privacy-policy/delta.md diff --git a/specs/changes/platform-security-privacy-policy/design.md b/specs/archive/2026-08-16-platform-security-privacy-policy/design.md similarity index 100% rename from specs/changes/platform-security-privacy-policy/design.md rename to specs/archive/2026-08-16-platform-security-privacy-policy/design.md diff --git a/specs/changes/platform-security-privacy-policy/evidence.md b/specs/archive/2026-08-16-platform-security-privacy-policy/evidence.md similarity index 95% rename from specs/changes/platform-security-privacy-policy/evidence.md rename to specs/archive/2026-08-16-platform-security-privacy-policy/evidence.md index 18f136c..9302f7b 100644 --- a/specs/changes/platform-security-privacy-policy/evidence.md +++ b/specs/archive/2026-08-16-platform-security-privacy-policy/evidence.md @@ -115,9 +115,11 @@ target-matrix gaps do not block unsupported preview distribution. 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 - 0.14.4 registry integrity, signature, and provenance remain pending publication. -- Result: pass for the public-preview pre-publish supply-chain prerequisite; incomplete for - 0.14.4 post-publish evidence and production readiness. + 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), diff --git a/specs/changes/platform-security-privacy-policy/proposal.md b/specs/archive/2026-08-16-platform-security-privacy-policy/proposal.md similarity index 100% rename from specs/changes/platform-security-privacy-policy/proposal.md rename to specs/archive/2026-08-16-platform-security-privacy-policy/proposal.md diff --git a/specs/changes/platform-security-privacy-policy/tasks.md b/specs/archive/2026-08-16-platform-security-privacy-policy/tasks.md similarity index 94% rename from specs/changes/platform-security-privacy-policy/tasks.md rename to specs/archive/2026-08-16-platform-security-privacy-policy/tasks.md index b6d4695..dfe254d 100644 --- a/specs/changes/platform-security-privacy-policy/tasks.md +++ b/specs/archive/2026-08-16-platform-security-privacy-policy/tasks.md @@ -10,4 +10,4 @@ - [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. -- [ ] Run repository validation and archive the packet. +- [x] Run repository validation and archive the packet. diff --git a/specs/changes/public-preview-release-gate/change.json b/specs/archive/2026-08-16-public-preview-release-gate/change.json similarity index 87% rename from specs/changes/public-preview-release-gate/change.json rename to specs/archive/2026-08-16-public-preview-release-gate/change.json index 2f27844..8567614 100644 --- a/specs/changes/public-preview-release-gate/change.json +++ b/specs/archive/2026-08-16-public-preview-release-gate/change.json @@ -3,7 +3,7 @@ "id": "public-preview-release-gate", "title": "Decouple public preview from certification evidence", "lane": "high-risk", - "status": "implementing", + "status": "archived", "affectedRequirements": [ "OUT-02", "OUT-03", @@ -16,7 +16,7 @@ "specs/current/governance-policy.spec.md", "specs/current/journey-evidence.spec.md" ], - "currentSpecsUpdated": false, + "currentSpecsUpdated": true, "approval": { "by": "bitgorust", "at": "2026-08-16T16:50:30Z" @@ -27,5 +27,5 @@ "reason": "" }, "createdAt": "2026-08-16", - "archivedAt": null + "archivedAt": "2026-08-16" } diff --git a/specs/changes/public-preview-release-gate/delta.md b/specs/archive/2026-08-16-public-preview-release-gate/delta.md similarity index 100% rename from specs/changes/public-preview-release-gate/delta.md rename to specs/archive/2026-08-16-public-preview-release-gate/delta.md diff --git a/specs/changes/public-preview-release-gate/design.md b/specs/archive/2026-08-16-public-preview-release-gate/design.md similarity index 100% rename from specs/changes/public-preview-release-gate/design.md rename to specs/archive/2026-08-16-public-preview-release-gate/design.md diff --git a/specs/changes/public-preview-release-gate/evidence.md b/specs/archive/2026-08-16-public-preview-release-gate/evidence.md similarity index 81% rename from specs/changes/public-preview-release-gate/evidence.md rename to specs/archive/2026-08-16-public-preview-release-gate/evidence.md index eb0e157..b67a1af 100644 --- a/specs/changes/public-preview-release-gate/evidence.md +++ b/specs/archive/2026-08-16-public-preview-release-gate/evidence.md @@ -3,8 +3,8 @@ 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; post-publication results remain pending, so the transition is -`preview-candidate` rather than `public-preview`. +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 @@ -35,7 +35,7 @@ npm trusted publisher; post-publication results remain pending, so the transitio - 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; exact release transition remains pending. +- Result: pass for the implemented contract and exact public-preview transition. - Evidence: [@test](test/release-integrity.test.ts), [@manual](docs/requirements-traceability.md) @@ -58,10 +58,12 @@ npm trusted publisher; post-publication results remain pending, so the transitio 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`; registry-byte, signature, and - provenance evidence is still pending. -- Result: pass for `preview-candidate`; `public-preview` remains blocked pending the exact tag - workflow and registry verification. + 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) @@ -72,6 +74,7 @@ npm trusted publisher; post-publication results remain pending, so the transitio - 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 candidate documentation; final release status remains pending. +- 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/changes/public-preview-release-gate/proposal.md b/specs/archive/2026-08-16-public-preview-release-gate/proposal.md similarity index 100% rename from specs/changes/public-preview-release-gate/proposal.md rename to specs/archive/2026-08-16-public-preview-release-gate/proposal.md diff --git a/specs/changes/public-preview-release-gate/tasks.md b/specs/archive/2026-08-16-public-preview-release-gate/tasks.md similarity index 78% rename from specs/changes/public-preview-release-gate/tasks.md rename to specs/archive/2026-08-16-public-preview-release-gate/tasks.md index a39379d..a82150a 100644 --- a/specs/changes/public-preview-release-gate/tasks.md +++ b/specs/archive/2026-08-16-public-preview-release-gate/tasks.md @@ -5,6 +5,6 @@ - [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. -- [ ] Publish only after the authorized preview candidate passes; retain registry-byte, signature, and provenance verification. -- [ ] Record validation and verification evidence and update affected `specs/current/` files. -- [ ] Run repository validation and archive the packet. +- [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/current/governance-policy.spec.md b/specs/current/governance-policy.spec.md index c5eb8d6..89e2411 100644 --- a/specs/current/governance-policy.spec.md +++ b/specs/current/governance-policy.spec.md @@ -36,9 +36,8 @@ - `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 then-current published release for its - own tag/commit; trusted- - publisher configuration and future registry provenance are not. The approved renderer + 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 @@ -46,5 +45,7 @@ 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. Preview history supplies no + 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.