diff --git a/AGENTS.md b/AGENTS.md index 0e889e8..15b9b2a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,7 @@ curbpack init # when ready — house-policy default curbpack check # exit code authoritative # red: curbpack check --heal && curbpack ask .github/curbpack/cache/latest_failure.json --propose # green: curbpack ask-my-suppliers # durable buyer checklist + pack draft +# green (share): curbpack share → hand review-pack/; reviewers: curbpack review (offline triage) # optional pathway sidecar (never gates check): curbpack pathway status # human next ask by default (--technical for phase path) # optional research sidecar (never gates check): curbpack research [--fetch] [--gate-id=…] @@ -41,7 +42,7 @@ trust-import · review-sign · Last tabletop: · confirm-* · attest · pin-bump 1. After doc/dep edits → run `curbpack check` (exit code authoritative). 2. On red → `curbpack check --heal` then `curbpack ask … --propose` — never invent certification; never auto-attest. -3. On green → optional `curbpack export --context-pack` / `--buyer-questions` for humans. +3. On green → optional `curbpack export --context-pack` / `--buyer-questions` for humans; reviewers receiving a share can run `curbpack review ` offline (document triage — not confirm/attest). 4. Prefer ContextPack + dual-rep IR over guessing cache files. 5. Pin Action / examples at **`@v0.5.2`**. Never claim CE / notified-body approval. 6. **Pathway:** call `curbpack pathway status|suggest|note` only — never forge `pathway-seed.json` or invent pack ids. Stop for human `confirm-*` and `attest`. Prefer ContextPack pathway next + RKG after confirm-packs; post-attest next is local proof verify (human). MCP never confirms/attests. Seed is not a gate input. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b665a1..92b302f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased (on `main`, not yet pinned — Action pin stays `@v0.5.2`) +- **Reader review wedge (comms + CLI)** — `curbpack review ` offline document triage (confirmed / unconfirmed / contradicted) on curbpack-native review-packs — not a product verdict. Teaching sample at `site/samples/review-pack/`; site page `site/receiving-submissions/` (“Are you receiving submissions?”). Home / for-reviewers promote artifact trust table + review path. Pin stays `@v0.5.2`. Not certification. +- **Phase 0-slim honesty** — installer fallback no longer prints a failing `go install …/RI-SE/curbpack` line; claim-safe guidance points to RI-SE binary releases / `docs/getting-started/install.md` and notes the Go module path remains `github.com/afelin/curbpack` until wave-2 (strangers: binary only). SDD banner + §8–§10 clarify **`curbpack verify` is not a shipped CLI verb** (future reader wedge: `curbpack review`). Directional evaluator tip-drift note + `scripts/evaluator-behavior-check.sh`. Pin stays `@v0.5.2`. Not certification. +- **Reader wedge (`curbpack review`)** — offline triage of a received curbpack-native `review-pack/` (no git, no network): confirmed / unconfirmed / contradicted on structure, digests, and references (allowlisted URLs recorded never fetched). Pasteable triage note; exit 1 on contradicted. Frozen sample: `testdata/sample-review-pack` + `site/samples/review-pack`. Site: reviewers-first home, receiving-submissions page, trust-table links. Phase 6 scaffold + post-kill-test gates for intake/lint/batch. Document triage only — not a product verdict. Pin stays `@v0.5.2`. - **Slice A HITL honesty** — structural evidence for human review — not certification and not a CRA-compliant / CE / notified-body claim. GateFailure IR + ContextPack populate optional `agent_id` / `model_hash` / `active_mandate_id` from env; `source` is `self-declared` vs `bridge` when the Coreward sock path is present; missing sock fail-opens (`not_installed` / `unavailable`) and does not fail check; AgentIdentity is not in `state_hash`; no new sock ops. `TestNoVerdictSurface` locks public renderers so compliant / merge-allow are not a Curbpack verdict (`readiness_score` stays on GateFailurePayload). Opt-in `cra-baseline` file gate `docs/incident/art14-path.md` (Art 14 reporting rehearsal vs later handling clock; not house-policy default). **`--diff` always runs `anti_placeholder`** (committed heal stub + unrelated README change still fails `HOUSE-ANTI-PLACEHOLDER` / `CRA-ANTI-PLACEHOLDER` with scaffold body overlap). **`pathway confirm-prose` requires every displayed prose path independent** (not one-of); always runs inward cite-check (repo artifact or allowlisted cite; heal stubs / empty / agent-cache are not grounding). **Existing `cra-baseline` greens go red until `docs/incident/art14-path.md` is real prose.** `--heal` remaining red is intended. `--i-am-human` / `CURBPACK_ALLOW_CONFIRM=1` unchanged. Counsel note: Art 14 reporting vs handling; AI Act Art 50 grace is not blanket. Example workflow: compose Trivy/Gitleaks beside check; never set `CURBPACK_ALLOW_CONFIRM=1` on the Action. Pin stays `@v0.5.2`. Pack catalog frozen (three ids). Trust-surface freeze continues (no Action resolve / SafeJoin / OCC / airlock / sock / pack catalog). - **Handoff honesty** — one-pager cover sheet (files-to-read front, gate score on the back); `anti_placeholder` fails DefaultScaffoldBody overlap (`--heal` remaining red is intended); drift `docs_changed_since_attest` / `docs_unchanged_since_attest` plus optional security.txt contact signals; medtech formhints guess path `docs/medtech/…`. Proof yes/no stamp copy; share prints `share_stale` first; doctor warns on `CURBPACK_ALLOW_CONFIRM=1`; optional attest `--reviewed-by` in evidence only. Structural evidence for human review — not certification. Pin stays `@v0.5.2`. Trust-surface freeze continues (no Action resolve / SafeJoin / OCC / airlock / sock / pack catalog). - **Premortem production fixes (PR #57)** — `init` gitignores cache/evidence; Action `heal` default **false** + scaffold≠readiness warning; pathway confirm requires `--i-am-human` / `CURBPACK_ALLOW_CONFIRM=1` (TTY alone refused); `LatestNoteCommit` walks notes (not HEAD-without-note); Action refuses Windows runners + red `REMEDIATION REVIEW` artifacts; claim-safety scans `*.ps1`; redteam **18/18**; maintainer playbook [`docs/getting-started/release-v0.5.2.md`](docs/getting-started/release-v0.5.2.md) diff --git a/CLAUDE.md b/CLAUDE.md index 3e4d2d7..2e46ef0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,6 +19,7 @@ curbpack check # curbpack pathway note --set last_draft_pick=A|B|edited # before confirm-prose: curbpack research --cite-check # green (optional share): curbpack export --context-pack +# reviewers (offline): curbpack review # document triage — not confirm/attest ``` ## Human-only acts diff --git a/docs/getting-started/buyer-evidence.md b/docs/getting-started/buyer-evidence.md index dad660c..00f6440 100644 --- a/docs/getting-started/buyer-evidence.md +++ b/docs/getting-started/buyer-evidence.md @@ -20,6 +20,14 @@ Suppliers may reach green via Write, Bring, or CI; that path is theirs. Optional | **Local gate score** | Not a certification score or market-access percentage | | **UNSIGNED** | Capsule may be present; not cryptographically verified | | **ssh-agent-signed** | Real SSH signature on the attest capsule | +| **`curbpack review` triage** | Offline document consistency (confirmed / unconfirmed / contradicted, with cause split) — not a product verdict | + +**Offline / locked laptop:** install via [Ladder 3 — manual binary + checksums](install.md#ladder-3--manual-binary--checksums) (receive binary as file + `checksums.txt`, verify fail-closed). Then: + +```bash +curbpack review +# terse default; --full for all findings + dropped tokens; --json for schema v2 +``` Use the full [artifact trust table](../../site/for-reviewers/) before equating any artifact with legal conformity or CE readiness. diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index 6cd1d48..43d4f89 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -89,6 +89,8 @@ Set-ExecutionPolicy -Scope CurrentUser RemoteSigned ### Ladder 3 — Manual binary + checksums +For locked-down or offline assessor laptops: receive the release binary **as a file** (email attachment, USB, or internal file drop) together with `checksums.txt`, verify sha256 fail-closed, then place on PATH — same steps as below. No installer required. See also [buyer evidence](buyer-evidence.md) and [for reviewers](../../site/for-reviewers/). + 1. Open the release for pin **`v0.5.4`**: https://github.com/RI-SE/curbpack/releases/tag/v0.5.4 2. Download the asset for your OS from the manifest (`curbpack_darwin_*`, `curbpack_linux_*`, or `curbpack_windows_amd64.exe`) **and** `checksums.txt`. 3. Verify sha256 (fail closed if mismatch). diff --git a/docs/internal/evaluator-tip-drift.md b/docs/internal/evaluator-tip-drift.md new file mode 100644 index 0000000..aa5d5d9 --- /dev/null +++ b/docs/internal/evaluator-tip-drift.md @@ -0,0 +1,32 @@ +# Evaluator tip drift (directional) + +> Maintainer note. Dual-remote mirror sync was removed (`scripts/curb-sync.sh` deprecated). **RI-SE/curbpack** is the sole public source of truth; **afelin/curbpack** is private development. + +## Behavior bar (do not re-derive) + +Vacuous-pass / not-started / target-absent semantics for tip evaluators are already locked by tests under `internal/validate`. Treat those cases as the behavior bar — do not invent a second definition of “aligned.” + +Relevant cases include: + +- `TestAntiPlaceholderTargetAbsentWhenAllMissing` — missing annex targets must **not** vacuous-pass `anti_placeholder` (target absent). +- `TestNPMDepBanVacuousPassPresentManifest` — present manifest with no banned pin may pass. +- `TestNPMDepBanTargetAbsentMissingManifest` — missing `package.json` must **not** vacuous-pass dep-ban (target absent). +- Related `anti_placeholder` stub / fresh-stub tests in the same package. + +`AllNotStarted` / `IsNotStartedFailure` in `internal/validate` classify scaffold / absent / not-started findings for TTY; tip comparison must respect that split (○ not-started ≠ ✘ fail ≠ silent green). + +Lean runner: [`scripts/evaluator-behavior-check.sh`](../../scripts/evaluator-behavior-check.sh). + +## Drift checks must be directional + +When comparing public tip to another tip (or a private development tip): + +1. Report **which side is ahead** (commit/date), not only whether hashes differ. +2. **Fail loudly** if evaluator behavior diverges (vacuous-pass / AllNotStarted / anti_placeholder target-absent semantics). +3. Do **not** treat hash-only mirror noise as a pass/fail signal — missing dual-remote sync means tip inequality alone is expected noise, not a gate. + +## Public SoR + +Public tip comparison is against **RI-SE `main`**. Private afelin work is development; it is not a second public mirror to keep byte-identical for its own sake. + +Not conformity assessment. Exit codes and local `curbpack check` remain authoritative for repo gates. diff --git a/docs/internal/historical-verify-target.md b/docs/internal/historical-verify-target.md new file mode 100644 index 0000000..8b24e1e --- /dev/null +++ b/docs/internal/historical-verify-target.md @@ -0,0 +1,36 @@ +# HISTORICAL / TARGET — `curbpack verify` sketches + +> **NOT SHIPPED.** `curbpack verify` is **not** a CLI verb. Do not run it; do not teach it as available. +> +> Recipient-side document triage is intended as future **`curbpack review`**. Do not resurrect `verify` as the ship name. +> +> Structural evidence for human review — not conformity assessment, not certification, not CE / notified-body opinion. + +These snippets were design-intent examples formerly presented without enough shipping fences in the SDD. Kept here so implementers can see the old shape without mistaking it for product surface. + +## Asker loop (historical wording) + +``` +asker publishes signed pack → supplier runs it locally → +supplier returns signed evidence → asker runs `curbpack verify` +``` + +**Today:** stop after signed evidence; humans review artifacts. **Target ship name:** `curbpack review`. + +## Operations sketch (historical) + +``` +curbpack packs sign +curbpack packs trust import # human-only; not built yet +curbpack verify # NOT SHIPPED — historical name only +``` + +## Artifact footer (historical) + +``` +Generated locally by curbpack. Nothing was uploaded. Verify it yourself: `curbpack verify `. +``` + +**Today:** do not print a `curbpack verify` invitation. Prefer claim-safe review language until `curbpack review` ships. + +See also: [software-design-document.md](../software-design-document.md) banner + §9.1; [sdd-gap-analysis.md](sdd-gap-analysis.md). diff --git a/docs/internal/phase6-kill-test.md b/docs/internal/phase6-kill-test.md new file mode 100644 index 0000000..6154a46 --- /dev/null +++ b/docs/internal/phase6-kill-test.md @@ -0,0 +1,92 @@ +# Phase 6 — kill-test scaffold + +> Maintainer note. Procedure scaffold only — results TBD. Not conformity assessment. Not certification. + +## Goal + +Falsify or support product value of offline **document triage** (`curbpack review` on curbpack-native review-packs): a useful mix of **confirmed** / **unconfirmed** / **contradicted** states across real submissions — reported as the **cause split**, never vanity summed `% unconfirmed` alone. + +**Variance required:** ~100% confirmed **or** ~100% unconfirmed across the public set **falsifies** product value (no discriminative signal). Report **distribution**, never name-and-shame projects. + +## Gate before cohort + +**Negative (sensitivity) + specificity PASS is required before any cohort / `--batch` kill-test n=10.** + +| Control | Pass rule | +|---------|-----------| +| Sensitivity matrix | Broken packs must detect: absent cited path → `unconfirmed`+`genuine`; altered `result_digest` / wrong sbom digest → `contradicted`+`self_disagree` | +| Specificity | Known-good bundle → `UnconfirmedGenuine == 0` **and** `ContradictedCount == 0` exactly | +| Extractor | Dogfood `UnconfirmedExtractor == 0` (non-zero is a classifier regression — fix before cohort) | + +P4 red → no cohort, no circular OSS share→review as sole kill-test. See tests in `internal/review`. + +## Procedure + +### 1. Dogfood curbpack-on-itself (split metrics) + +1. Run the existing dogfood path: [`.github/workflows/curbpack-dogfood.yml`](../../.github/workflows/curbpack-dogfood.yml) (Action against this repo; local equivalent: build binary → `curbpack check` / share path as documented). +2. If a local curbpack-native **review-pack** directory is present (e.g. after prepare-release / share; often gitignored as `/review-pack/`), run: + + ```bash + curbpack review --full + ``` + +3. Record **split** counts: confirmed; unconfirmed by cause (`producer` / `extractor` / `genuine` / `external`); contradicted by cause (`self_disagree` / …). Document triage ≠ product verdict. + +### 2. Ten public OSS projects (only after P4 green) + +1. Select **ten** public open-source projects that publish **security documentation** (e.g. SECURITY.md, security.txt, disclosure policy — claim-safe selection criteria only). +2. For each, produce or obtain a curbpack-native review-pack via the normal local loop (scan/init/check/share as applicable on a clone) — **on the evaluator’s machine**; do not upload proprietary trees. +3. Prefer **independent docs+code or intake-completeness** pilots when negatives fire and specificity holds — do **not** treat circular OSS share→review as the sole kill-test. +4. Run `curbpack review ` (or `--batch` over prepared dirs) on each pack. +5. Aggregate **only** the distribution of split counts across the ten. **Do not** publish a per-project shame table. Internal worksheets may keep ids; public write-ups stay anonymized aggregates. + +### 3. Named org case study + +A **named** organization case study requires **prior written consent**. Without consent: keep anonymized or omit. + +## Cohort decision (after dogfood + P4) + +| Observation | Decision | +|-------------|----------| +| Negatives fire; specificity holds; dogfood genuine low | Detector works; prefer independent docs+code or intake-completeness pilot | +| Sensitivity or specificity fails | No cohort | +| `extractor` non-zero on dogfood | Classifier regression — fix first | + +Defer `intake` / `packs lint` per [post-kill-test-gates.md](post-kill-test-gates.md). + +## Results table (placeholder) + +| Cohort | n | confirmed | unconfirmed split (P/E/G/X) | contradicted | Notes | +|--------|---|-----------|-----------------------------|--------------|-------| +| Self dogfood (this repo `review-pack/`) | 1 | 23 | 0 / 0 / 0 / 0 | 0 | After digest-fingerprint + triage-surface harden: digests confirmed; extractor 0; genuine 0 (no cache-path flood) | +| Frozen sample fixture | 1 | 10 | 7 / 0 / 1 / 1 | 0 | `testdata/sample-review-pack` — aha path; extractor 0; genuine=SECURITY.md cite | +| Public OSS (security docs) | 10 | TBD | TBD | TBD | **Blocked until human records dogfood + decides cohort type** | +| Named org (consent) | TBD | TBD | TBD | TBD | Omit until written consent | + +**P4 controls (automated):** sensitivity matrix + specificity known-good — PASS in `internal/review` (`TestSensitivityMatrix`, `TestSpecificityKnownGood`). Extractor on sample = 0. + +**Cohort decision (process):** Prefer independent docs+code or intake-completeness pilot once a human records self-dogfood split metrics; do **not** use circular OSS share→review as the sole kill-test. Intake/lint remain deferred. + +## Public OSS cohort (internal worksheet — consent-free) + +Ten public projects with published security documentation (selection only; results stay aggregated): + +1. curl/curl +2. openssl/openssl +3. golang/go +4. kubernetes/kubernetes +5. torvalds/linux (security docs only — expect heavy; optional substitute: aquasecurity/trivy) +6. sigstore/cosign +7. aquasecurity/trivy +8. github/codeql-action +9. rustls/rustls +10. python/cpython + +Procedure per clone: shallow clone → `curbpack init --yes` (or house profile) → `check` / `share` as far as the tree allows → `curbpack review ` → record **split** counts only. Never publish a per-project shame table. + +## Claim discipline + +- Triage is about the **received document**, not legal conformity. +- Never equate kill-test metrics with CE, notified-body, or CRA compliance. +- Pin stays as documented in AGENTS.md until a human approves a bump. diff --git a/docs/internal/post-kill-test-gates.md b/docs/internal/post-kill-test-gates.md new file mode 100644 index 0000000..fbd5128 --- /dev/null +++ b/docs/internal/post-kill-test-gates.md @@ -0,0 +1,24 @@ +# Post kill-test gates (compress-after) + +> Maintainer note. Structural evidence for human review — not conformity assessment. + +## Rule + +**Intake** and **packs lint** stay **gated** until either: + +1. **Phase 6 kill-test** completes with an acceptable outcome (see [phase6-kill-test.md](phase6-kill-test.md)), **or** +2. An **explicit human risk accept** is recorded (written maintainer decision — not an agent invent, not a chat greenlight). + +`curbpack review --batch` is available after **sensitivity + specificity** controls pass (see phase6 gate table) for ranked local triage. It is **not** a greenlight for intake/lint or public cohort advice. + +Do **not** implement intake or packs-lint expansion while this gate is closed. + +## Why + +The reader wedge (`curbpack review` + review-pack triage) must prove distributional value on real packs before we widen intake surface. Shipping intake early creates operational load without a falsifiable product signal. + +## Out of scope here + +- Implementing intake / packs lint +- Pin bumps, trust-import, attest, or HPURL work +- Claiming certification or CE / notified-body outcomes from kill-test metrics diff --git a/docs/internal/sdd-gap-analysis.md b/docs/internal/sdd-gap-analysis.md index 4b02af5..6547e9f 100644 --- a/docs/internal/sdd-gap-analysis.md +++ b/docs/internal/sdd-gap-analysis.md @@ -28,7 +28,7 @@ |----------|------------| | `internal/report` | Missing — findings re-declared across ~8 structures | | `internal/sign` | Missing — attest uses ssh-agent notes, not `ssh-keygen -Y` pack verify | -| `verify` command | Missing — proof page is client-side hash compare only | +| `verify` command | **Missing** — **`curbpack verify` is not a shipped CLI verb**; proof page is client-side hash compare only. Intended reader wedge name: future `curbpack review` (document triage). See [historical-verify-target.md](historical-verify-target.md). | | `references` check primitive | Missing | | Check-kind registry (§6.3) | Partial — `checkRegistry` map in `checks.go`, not unified `CheckKind` descriptor | | `packs init --from-repo` | Missing | diff --git a/docs/migration-cyberready-to-curbpack.md b/docs/migration-cyberready-to-curbpack.md index cadc87d..1debd41 100644 --- a/docs/migration-cyberready-to-curbpack.md +++ b/docs/migration-cyberready-to-curbpack.md @@ -47,11 +47,9 @@ curl -fsSL https://raw.githubusercontent.com/RI-SE/curbpack/main/scripts/install curb doctor && curb demo ``` -From source: +From source (maintainers / clone holders only): -```bash -go install github.com/afelin/curbpack/cmd/curbpack@v0.5.0 -``` +The Go module path remains `github.com/afelin/curbpack` until wave-2 migration. With the development repo private, strangers should use the binary installers above — do not treat `go install github.com/RI-SE/curbpack/...` as a working path (module path differs), and do not rely on `go install github.com/afelin/curbpack/...` as a public install. ## Pedagogy (unchanged gates) diff --git a/docs/software-design-document.md b/docs/software-design-document.md index c637de1..2335ab8 100644 --- a/docs/software-design-document.md +++ b/docs/software-design-document.md @@ -1,12 +1,12 @@ # Curbpack — Software Design Document -> **Target state:** This document describes design intent — not everything here is shipped yet (e.g. `verify` is not in the CLI). For repository baseline vs this doc, see [internal SDD gap analysis](internal/sdd-gap-analysis.md). +> **Target state — not a full shipped command catalog.** This document describes design intent. **`curbpack verify` is not a shipped CLI verb** (do not run it; do not resurrect that name). Recipient-side document triage **is shipped** as **`curbpack review `** (offline; confirmed / unconfirmed / contradicted — document only, not a product verdict). Nothing in this doc is certification, CE marking, or notified-body opinion. For repository baseline vs this doc, see [internal SDD gap analysis](internal/sdd-gap-analysis.md). Historical / target-only `verify` sketches: [internal/historical-verify-target.md](internal/historical-verify-target.md). **Version:** 1.0 · **Date:** 19 August 2026 · **Status:** target state **Applies to:** `github.com/RI-SE/curbpack` **Audience:** implementers and the agents working this repository -This document describes the state curbpack is being built toward — the design, the reasoning behind it, and the work required to get there. It is the single source of design intent. Where it conflicts with older notes, plans or specifications, this document wins. +This document describes the state curbpack is being built toward — the design, the reasoning behind it, and the work required to get there. It is the single source of design intent. Where it conflicts with older notes, plans or specifications, this document wins. **Shipped verbs** live in the CLI help / `AGENTS.md` loop — not in the aspirational tables below. Two lists in this document are load-bearing and must not be confused: @@ -271,10 +271,12 @@ This closes the loop with no new infrastructure: ``` asker publishes signed pack → supplier runs it locally → -supplier returns signed evidence → asker runs `curbpack verify` +supplier returns signed evidence → asker runs recipient triage + (TARGET ONLY — not shipped: historically sketched as `curbpack verify`; + intended ship name for document triage is `curbpack review`) ``` -No questionnaire round-trip, no portal, no registry. +No questionnaire round-trip, no portal, no registry. Not conformity assessment. --- @@ -288,10 +290,13 @@ Signing and verification use **OpenSSH signatures and `allowed_signers`**. No be ### 8.2 Operations +> **TARGET / NOT SHIPPED:** the `verify` lines below are design sketches only. **`curbpack verify` is not a CLI verb today.** Recipient document triage is intended as future **`curbpack review`**. Pack signing / trust-import remain Wave B. Not certification. + ``` -curbpack packs sign # ssh-keygen -Y sign -n -curbpack packs trust import # HUMAN-ONLY — see §12 -curbpack verify # ssh-keygen -Y verify +curbpack packs sign # TARGET: ssh-keygen -Y sign -n +curbpack packs trust import # HUMAN-ONLY — see §12; not built yet +# NOT SHIPPED — do not run: +# curbpack verify # historical name; prefer future: curbpack review ``` ### 8.3 Verdict states @@ -349,18 +354,20 @@ The verb count does not grow with regimes or stakeholders. Each stakeholder uses | `init` | Configuration, hooks, editor integration — offered after first value | Yes | | `check` | Evaluate; exit code authoritative | Cache only | | `ask-my-suppliers` | Emit the question set and the pack | Yes, to a durable path | -| **`verify`** | Verify a received artifact | **No** | +| ~~`verify`~~ → **`review`** *(TARGET)* | Recipient document triage / artifact check | **No** — **not a shipped CLI verb**; do not run `curbpack verify` | | `packs init --from-repo` | Author a pack by example | Yes, one file | | `packs sign` / `packs trust import` | Sign a pack / import signers | Yes | | `export --` | SARIF, context pack, buyer questions | Yes | | `attest` | Human-only capsule | Yes | | `doctor` | Environment diagnosis | No | -### 9.1 `verify` — why it exists +### 9.1 Recipient triage (`review`) — why it exists *(SHIPPED)* + +> **Shipped:** `curbpack review ` triages a curbpack-native review-pack offline (no git, no network). States are **confirmed** / **unconfirmed** / **contradicted** about the **document** — never a product verdict. Historical SDD text used the verb name `verify`; **do not implement `curbpack verify`**. Not certification, not a conformity gate. Curbpack today serves producers. Buyers, assessors and distributors receive HTML they cannot check. One read-only verb converts every artifact recipient into a potential installer — and the recipient is the party deciding whether a supplier keeps the contract. A producer install serves one repository; a recipient install serves every supplier that recipient has. -Output: the verdict, which claims resolve to real artifacts, which do not, and what could not be checked. Offline, no account. +Output: the verdict, which claims resolve to real artifacts, which do not, and what could not be checked. Offline, no account. Structural evidence for human review only. --- @@ -371,7 +378,7 @@ Every artifact that leaves the building carries, without exception: - the computed `assurance_class` and **"mechanically evidenced: X of Y"**; - the verification verdict, in the vocabulary of §8.3; - the claim boundary: *prepares evidence for human review — not a conformity assessment*; -- the footer: *Generated locally by curbpack. Nothing was uploaded. Verify it yourself: `curbpack verify `.* +- the footer: *Generated locally by curbpack. Nothing was uploaded. Review the artifact yourself (TARGET: future `curbpack review` — **`curbpack verify` is not shipped**).* That footer converts a claim into an invitation, which is both the honest framing and the distribution mechanism. @@ -489,7 +496,7 @@ Each wave states a precondition. Work that cannot name its consumer does not sta |---|---| | Check-kind registry (§6.3) | 1 d | | `references` (§6.1) | 2 d | -| `verify` (§9.1) | 2 d | +| Recipient triage / `review` (§9.1; historical name `verify`) | 2 d | | Signing, trust import, three verdict states (§8) | 1 d | | `packs init --from-repo` (§7.1) | 2 d | | Computed `assurance_class`, reviewer attestation, self-contained bundle | 1.5 d | diff --git a/docs/stable-contracts.md b/docs/stable-contracts.md index 35154ea..a3fd8b9 100644 --- a/docs/stable-contracts.md +++ b/docs/stable-contracts.md @@ -83,4 +83,19 @@ Frozen ops (unchanged): `validate_delta`, `get_latest_failure`, `graph_summary`, `curbpack share --bundle` writes `review-pack/evidence-bundle.html` with ``, optional REMEDIATION banner on red gates, and embedded hpurl pointer JSON for offline verify. -See also: [Strategy boundary](strategy-boundary.md) · [Coreward bridge](coreward-bridge.md) · [Security model](security-model.md) +## Review report (`curbpack review`) + +Offline document triage of a received curbpack-native review-pack. **Not** a product verdict or conformity assessment. No current MCP / `exportx` consumer — CLI-local assessor surface; treat JSON as the product contract for future intake. + +| Contract | Rule | +|----------|------| +| Schema | `curbpack-review-report:2` (`--json`) | +| Classifier | `classifier_version` string (e.g. `refclass:1`) — golden list; changes that move the reference denominator must be visible | +| States | `confirmed` \| `unconfirmed` \| `contradicted` only | +| Additive `cause` | Unconfirmed: `producer` \| `extractor` \| `genuine` \| `external`. Contradicted: `self_disagree` (digest/structure contradictions) | +| Counters | Split unconfirmed/contradicted-by-cause fields; `dropped_count` (+ `dropped` under `--full`) | +| Digests | Producer emits payload/file digests; bind disagreements appear as sibling `*_bind` keys — reader contradicts, never silently prefers bind | +| Airlock | Redact-then-emit home-path/PEM in findings, then fail closed via `PacketLooksAirlocked` | +| Exit | `1` if any contradicted (or `--batch` child unreadable/contradicted); usage → `2`. `--batch --full` / `--batch --json` → usage | + +See also: [Strategy boundary](strategy-boundary.md) · [Coreward bridge](coreward-bridge.md) · [Security model](security-model.md) · [Phase 6 kill-test](internal/phase6-kill-test.md) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 71fc92e..9852288 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -139,6 +139,7 @@ func usage() { fmt.Fprintf(os.Stderr, " ask-my-suppliers [--stdout-only] [--out path]\n") fmt.Fprintf(os.Stderr, " Supplier checklist → stdout + review-pack/ (writes files)\n") fmt.Fprintf(os.Stderr, " share [--bundle] [--reveal] check → context-pack → buyer-questions → prepare-release\n") + fmt.Fprintf(os.Stderr, " review Offline triage of a received review-pack (no git / no network)\n") fmt.Fprintf(os.Stderr, " drift [--json] Multi-signal evidence checklist (exit 0 always)\n") fmt.Fprintf(os.Stderr, " prepare-release Review-pack + evidence\n") fmt.Fprintf(os.Stderr, " attest [--allow-dirty] [--reviewed-by=Name] Human Git Notes capsule (then proof verify)\n\n") @@ -150,6 +151,7 @@ func usage() { fmt.Fprintf(os.Stderr, " export --sarif|--explain-packet|--watchlist-join|--buyer-questions|--lay-of-land|--context-pack [--spdx] [--slsa]\n") fmt.Fprintf(os.Stderr, " Standards / airlock / buyer checklist / instrument map / ContextPack\n") fmt.Fprintf(os.Stderr, " share [--bundle] [--reveal] Recipe + optional Explorer/Finder reveal\n") + fmt.Fprintf(os.Stderr, " review [--json] Received-pack triage: confirmed / unconfirmed / contradicted\n") fmt.Fprintf(os.Stderr, " drift [--json] Evidence drift checklist (informational; exit 0)\n") fmt.Fprintf(os.Stderr, " pathway status|suggest|confirm-packs|confirm-prose|confirm-share|note\n") fmt.Fprintf(os.Stderr, " Warm-start seed + HITL ticks + session notes (sole writer of pathway-seed.json)\n") @@ -159,6 +161,7 @@ func usage() { fmt.Fprintf(os.Stderr, " view Show attest capsule for HEAD\n\n") fmt.Fprintf(os.Stderr, "Exit codes: 0=success/soft-ok 1=gates/error 2=usage/env (incl. doctor --repair missing binary)\n") fmt.Fprintf(os.Stderr, " scan uses exit 0 when diagnosis completes, even when findings remain.\n") + fmt.Fprintf(os.Stderr, " review exits 1 when any finding is contradicted (document triage, not product verdict).\n") fmt.Fprintf(os.Stderr, " doctor may report advisory issues without signalling gate failure.\n") fmt.Fprintf(os.Stderr, " Only check provides repository gate pass/fail.\n") } diff --git a/internal/cli/completion.go b/internal/cli/completion.go index d62540a..5bae50e 100644 --- a/internal/cli/completion.go +++ b/internal/cli/completion.go @@ -10,7 +10,7 @@ var completionCommands = []string{ "help", "version", "doctor", "demo", "scan", "fix", "init", "check", "validate", "ask-my-suppliers", "prepare-release", "packs", "ask", "attest", "view", - "export", "share", "pathway", "research", "drift", "completion", + "export", "share", "review", "pathway", "research", "drift", "completion", } var completionExportFlags = []string{ diff --git a/internal/cli/help.go b/internal/cli/help.go index bab437c..48c9e07 100644 --- a/internal/cli/help.go +++ b/internal/cli/help.go @@ -26,6 +26,15 @@ func commandUsage(cmd string) { case "share": fmt.Fprintf(os.Stderr, "Usage: curbpack share [--bundle] [--reveal] [--packs a,b] [--skip-prepare-release]\n") fmt.Fprintf(os.Stderr, " Recipe: check → context-pack → buyer-questions → prepare-release.\n") + case "review": + fmt.Fprintf(os.Stderr, "Usage: curbpack review [--json] [--full]\n") + fmt.Fprintf(os.Stderr, " curbpack review --batch …\n") + fmt.Fprintf(os.Stderr, " Offline triage of a curbpack-native review-pack (no git, no network).\n") + fmt.Fprintf(os.Stderr, " States: confirmed | unconfirmed | contradicted — document only, not a product verdict.\n") + fmt.Fprintf(os.Stderr, " Default output is terse; --full dumps all findings + dropped tokens; --json emits schema v2.\n") + fmt.Fprintf(os.Stderr, " --batch expands a non-pack parent to immediate child packs; ranks unreadable, then contradicted, then genuine-desc.\n") + fmt.Fprintf(os.Stderr, " --batch does not combine with --full or --json (run per-pack with --full / --json).\n") + fmt.Fprintf(os.Stderr, " Exit 1 if any finding is contradicted (or any --batch child is contradicted/unreadable); exit 2 on usage errors.\n") case "drift": fmt.Fprintf(os.Stderr, "Usage: curbpack drift [--json]\n") fmt.Fprintf(os.Stderr, " Multi-signal evidence checklist — informational; exit 0 always.\n") diff --git a/internal/cli/registry.go b/internal/cli/registry.go index 493f189..fc67ba5 100644 --- a/internal/cli/registry.go +++ b/internal/cli/registry.go @@ -24,6 +24,7 @@ var commandRegistry = []commandEntry{ {name: "demo", handler: cmdDemo}, {name: "export", handler: cmdExport}, {name: "share", handler: cmdShare}, + {name: "review", handler: cmdReview}, {name: "drift", handler: cmdDrift}, {name: "pathway", handler: cmdPathway}, {name: "research", handler: cmdResearch}, diff --git a/internal/cli/review.go b/internal/cli/review.go new file mode 100644 index 0000000..25474bc --- /dev/null +++ b/internal/cli/review.go @@ -0,0 +1,206 @@ +package cli + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/afelin/curbpack/internal/review" + "github.com/afelin/curbpack/internal/tty" +) + +func cmdReview(args []string) error { + jsonOut := false + full := false + batch := false + var paths []string + for i := 0; i < len(args); i++ { + a := args[i] + switch { + case a == "-h" || a == "--help": + return helpShownErr("review") + case a == "--json": + jsonOut = true + case a == "--full": + full = true + case a == "--batch": + batch = true + case strings.HasPrefix(a, "-"): + return usageErr("unknown flag for review: " + a) + default: + paths = append(paths, a) + } + } + if len(paths) == 0 { + commandUsage("review") + return usageErr("review requires a path to a received review-pack directory") + } + if batch { + if jsonOut { + return usageErr("review --batch does not combine with --json (run per-child with --json)") + } + if full { + return usageErr("batch prints rank lines only; run per-pack with --full") + } + return runReviewBatch(paths) + } + if len(paths) != 1 { + return usageErr("review accepts a single pack directory (use --batch for many)") + } + + tty.PrintHeader("curbpack review") + fmt.Fprintf(os.Stderr, "%s\n", tty.C(tty.Dim, "Offline document triage — not a product verdict.")) + + rep, err := review.Run(review.Options{ + BundleRoot: paths[0], + Writer: os.Stdout, + JSONOut: jsonOut, + Full: full, + }) + if err != nil { + return usageErr(err.Error()) + } + if review.HasContradictions(rep) { + fmt.Fprintf(os.Stderr, "%s\n", tty.C(tty.Yellow, "Contradicted findings present — see triage note above.")) + return gatesErr() + } + return nil +} + +type batchRow struct { + path string + base string + rep review.Report + err error + unreadable bool + contradict bool + genuine int +} + +func runReviewBatch(paths []string) error { + tty.PrintHeader("curbpack review --batch") + fmt.Fprintf(os.Stderr, "%s\n", tty.C(tty.Dim, "Offline document triage — not a product verdict.")) + + children := expandBatchPaths(paths) + if len(children) == 0 { + return usageErr("review --batch found no review-pack directories") + } + + var rows []batchRow + for _, p := range children { + row := batchRow{path: p, base: filepath.Base(p)} + rep, err := review.Run(review.Options{ + BundleRoot: p, + Writer: ioDiscard{}, + }) + if err != nil { + row.err = err + row.unreadable = true + } else { + row.rep = rep + row.contradict = review.HasContradictions(rep) + row.genuine = rep.UnconfirmedGenuine + } + rows = append(rows, row) + } + + sort.SliceStable(rows, func(i, j int) bool { + // (1) unreadable / parse-fail first, (2) contradictions, (3) genuine desc, (4) basename + if rows[i].unreadable != rows[j].unreadable { + return rows[i].unreadable + } + if rows[i].contradict != rows[j].contradict { + return rows[i].contradict + } + if rows[i].genuine != rows[j].genuine { + return rows[i].genuine > rows[j].genuine + } + return rows[i].base < rows[j].base + }) + + anyBad := false + for _, r := range rows { + status := "ok" + extra := "" + switch { + case r.unreadable: + status = "UNREADABLE" + extra = r.err.Error() + anyBad = true + case r.contradict: + status = "CONTRADICTED" + extra = fmt.Sprintf("%d contradicted · %d genuine", r.rep.ContradictedCount, r.rep.UnconfirmedGenuine) + anyBad = true + default: + extra = fmt.Sprintf("%d genuine · %d extractor · confirmed %d", + r.rep.UnconfirmedGenuine, r.rep.UnconfirmedExtractor, r.rep.ConfirmedCount) + } + fmt.Printf("%s\t%s\t%s\n", status, r.base, extra) + } + if anyBad { + return gatesErr() + } + return nil +} + +// looksLikeReviewPack reports whether dir has the required triage layers. +func looksLikeReviewPack(dir string) bool { + for _, name := range []string{"01-gate-failures.json", "buyer-onepager.html"} { + st, err := os.Stat(filepath.Join(dir, name)) + if err != nil || st.IsDir() || st.Size() == 0 { + return false + } + } + return true +} + +// expandBatchPaths expands each arg: a pack dir is used as-is; a non-pack directory +// expands to immediate child dirs that look like review-packs. Junk children are ignored. +func expandBatchPaths(paths []string) []string { + seen := map[string]struct{}{} + var out []string + add := func(p string) { + p = filepath.Clean(p) + if _, ok := seen[p]; ok { + return + } + seen[p] = struct{}{} + out = append(out, p) + } + for _, p := range paths { + p = filepath.Clean(p) + st, err := os.Stat(p) + if err != nil || !st.IsDir() { + // Keep unreadable/non-dir paths so batch can mark UNREADABLE. + add(p) + continue + } + if looksLikeReviewPack(p) { + add(p) + continue + } + entries, err := os.ReadDir(p) + if err != nil { + add(p) + continue + } + foundChild := false + for _, e := range entries { + if !e.IsDir() { + continue + } + child := filepath.Join(p, e.Name()) + if looksLikeReviewPack(child) { + add(child) + foundChild = true + } + } + if !foundChild { + // Parent was not a pack and had no pack children — surface as unreadable child. + add(p) + } + } + return out +} diff --git a/internal/cli/review_batch_test.go b/internal/cli/review_batch_test.go new file mode 100644 index 0000000..11f1495 --- /dev/null +++ b/internal/cli/review_batch_test.go @@ -0,0 +1,123 @@ +package cli + +import ( + "io" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestExpandBatchPaths_parentChildren(t *testing.T) { + parent := t.TempDir() + writeMinimalPack(t, filepath.Join(parent, "pack-a")) + writeMinimalPack(t, filepath.Join(parent, "pack-b")) + junk := filepath.Join(parent, "junk") + if err := os.MkdirAll(junk, 0o755); err != nil { + t.Fatal(err) + } + _ = os.WriteFile(filepath.Join(junk, "readme.txt"), []byte("nope\n"), 0o644) + + got := expandBatchPaths([]string{parent}) + if len(got) != 2 { + t.Fatalf("want 2 pack children, got %d: %v", len(got), got) + } + bases := map[string]bool{} + for _, p := range got { + bases[filepath.Base(p)] = true + } + if !bases["pack-a"] || !bases["pack-b"] { + t.Fatalf("want pack-a and pack-b, got %v", got) + } + if bases["junk"] { + t.Fatal("junk dir must be ignored") + } +} + +func TestExpandBatchPaths_singlePack(t *testing.T) { + dir := t.TempDir() + pack := filepath.Join(dir, "one") + writeMinimalPack(t, pack) + got := expandBatchPaths([]string{pack}) + if len(got) != 1 || got[0] != filepath.Clean(pack) { + t.Fatalf("pack dir must be single child, got %v", got) + } +} + +func TestCmdReviewBatch(t *testing.T) { + parent := t.TempDir() + writeMinimalPack(t, filepath.Join(parent, "alpha")) + writeMinimalPack(t, filepath.Join(parent, "beta")) + _ = os.MkdirAll(filepath.Join(parent, "noise"), 0o755) + + stdout, _ := captureReview(t, func() { + err := Run([]string{"review", "--batch", parent}) + // Producer-unconfirmed optional layers are fine; contradictions exit gates. + if err != nil && ExitCode(err) != ExitOK && ExitCode(err) != ExitGates { + t.Fatalf("unexpected exit: %v code=%d", err, ExitCode(err)) + } + }) + if !strings.Contains(stdout, "alpha") || !strings.Contains(stdout, "beta") { + t.Fatalf("batch ranks must name both packs: %q", stdout) + } + if strings.Contains(stdout, "noise") { + t.Fatalf("junk child must not appear: %q", stdout) + } + + err := Run([]string{"review", "--batch", parent, "--full"}) + if ExitCode(err) != ExitUsage { + t.Fatalf("--batch --full must usage-exit, got %d (%v)", ExitCode(err), err) + } + if err == nil || !strings.Contains(err.Error(), "batch prints rank lines only") { + t.Fatalf("--batch --full message missing: %v", err) + } +} + +func writeMinimalPack(t *testing.T, dir string) { + t.Helper() + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + payload := `{"schema_version":"1","pack_id":"house-policy","readiness_score":80}` + "\n" + _ = os.WriteFile(filepath.Join(dir, "01-gate-failures.json"), []byte(payload), 0o644) + _ = os.WriteFile(filepath.Join(dir, "02-action-report.md"), []byte("ok\n"), 0o644) + _ = os.WriteFile(filepath.Join(dir, "03-executive-summary.md"), []byte("ok\n"), 0o644) + _ = os.WriteFile(filepath.Join(dir, "buyer-onepager.html"), []byte( + `
Rule packs
house-policy
`, + ), 0o644) +} + +func captureReview(t *testing.T, fn func()) (stdout, stderr string) { + t.Helper() + oldOut, oldErr := os.Stdout, os.Stderr + rOut, wOut, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + rErr, wErr, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + os.Stdout, os.Stderr = wOut, wErr + defer func() { + os.Stdout, os.Stderr = oldOut, oldErr + }() + doneOut := make(chan string) + doneErr := make(chan string) + go func() { + b, _ := io.ReadAll(rOut) + doneOut <- string(b) + }() + go func() { + b, _ := io.ReadAll(rErr) + doneErr <- string(b) + }() + fn() + _ = wOut.Close() + _ = wErr.Close() + stdout = <-doneOut + stderr = <-doneErr + _ = rOut.Close() + _ = rErr.Close() + return stdout, stderr +} diff --git a/internal/exportx/testdata/goldens/buyer-onepager.html b/internal/exportx/testdata/goldens/buyer-onepager.html index f2bb9b8..ab79137 100644 --- a/internal/exportx/testdata/goldens/buyer-onepager.html +++ b/internal/exportx/testdata/goldens/buyer-onepager.html @@ -4,7 +4,7 @@ Curbpack — Buyer One-Pager - +