Skip to content

Commit 46e429f

Browse files
[codex] implement review-pack v0.4 (#1)
* implement review-pack v0.4 * address copilot review feedback
1 parent 95797a6 commit 46e429f

16 files changed

Lines changed: 1064 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## v0.4.0 - 2026-06-19
4+
5+
### Added
6+
7+
- Added `review-pack --input DIR --format json|markdown [--output FILE]` for consolidated local review of descriptor, PSBT, transaction, script, policy, and notes artifacts.
8+
- Added schema version `0.4` `ReviewPackReport` output with detected artifacts, per-artifact summaries, consolidated risk, warnings, missing data, cross-artifact findings, review questions, and limitations.
9+
- Added cross-artifact checks for descriptor/PSBT multisig and timelock signals, descriptor threshold limitations, and PSBT/transaction input-output counts.
10+
- Added review-pack fixtures and CLI regression tests for JSON stdout and Markdown file output.
11+
12+
### Security
13+
14+
- `review-pack` performs local file analysis only. It does not sign, create wallets, handle keys, broadcast transactions, or make network calls.
15+
316
## v0.3.0 - 2026-06-18
417

518
### Added

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "btc-risk-lab"
3-
version = "0.3.0"
3+
version = "0.4.0"
44
edition = "2021"
55
authors = ["Jose Robles"]
66
description = "A Rust CLI for explainable Bitcoin transaction, PSBT, and script risk reports."

README.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Bitcoin and Web3 systems often fail at the edges: incomplete transaction context
2121
- explicit missing-data dependencies
2222
- human-readable warning explanations
2323
- JSON output suitable for downstream automation
24+
- consolidated review packs for descriptor, PSBT, transaction, script, policy, and notes directories
2425
- optional AI summary layer that never replaces the technical report
2526
- clear security boundaries around private keys and funds
2627

@@ -34,6 +35,7 @@ This MVP is intentionally small, but it is structured like a real due diligence
3435
- script inspection heuristics
3536
- structured reporting with `serde`
3637
- Markdown and JSON output
38+
- review-pack reports with cross-artifact checks
3739
- CI with `fmt`, `clippy`, and tests
3840
- a security posture that avoids custody, signing, seed phrases, and private key handling
3941

@@ -107,6 +109,29 @@ Analyze an output descriptor:
107109
btc-risk-lab analyze-descriptor --descriptor "wsh(sortedmulti(2,02...,03...,04...))" --format markdown
108110
```
109111

112+
Analyze a local review pack directory:
113+
114+
```bash
115+
btc-risk-lab review-pack --input tests/fixtures/review-packs/complete --format markdown
116+
```
117+
118+
Write a review pack report to a file:
119+
120+
```bash
121+
btc-risk-lab review-pack --input ./review-pack --format json --output review-pack-report.json
122+
```
123+
124+
`review-pack` looks for these optional files:
125+
126+
- `descriptor.txt`
127+
- `psbt.base64`
128+
- `tx.json`
129+
- `script.txt`
130+
- `policy.json`
131+
- `notes.md`
132+
133+
It reuses the existing descriptor, PSBT, transaction, and script analyzers, then emits a schema `0.4` `ReviewPackReport` with detected artifacts, per-artifact summaries, consolidated risk, warnings, missing data, cross-artifact findings, review questions, and limitations.
134+
110135
Generate an optional executive summary from an existing JSON report:
111136

112137
```bash
@@ -172,6 +197,7 @@ Current analysis includes:
172197
- descriptor sanity check through `miniscript`
173198
- descriptor max satisfaction weight where available
174199
- threshold and multisig policy hints
200+
- review-pack cross-artifact checks for descriptor/PSBT policy signals and PSBT/transaction input-output counts
175201
- script complexity score
176202
- report schema versioning
177203
- missing-data dependencies
@@ -188,6 +214,7 @@ Current analysis includes:
188214
- handle private keys
189215
- request seed phrases
190216
- broadcast transactions
217+
- make network calls from `review-pack`
191218
- promise consensus-level validation
192219
- send secrets to an LLM
193220

@@ -203,7 +230,9 @@ AI support is optional and isolated behind the `ai` feature flag. The intended p
203230

204231
This MVP uses heuristics. It does not perform full Bitcoin Core policy validation, mempool acceptance simulation, chain lookup, script execution, wallet state analysis, or consensus-level validation.
205232

206-
Risk classifications are only as complete as the artifact data provided. Missing UTXO data, omitted redeem scripts, absent witness scripts, incomplete PSBT maps, and descriptors without operational wallet context can all reduce confidence.
233+
Risk classifications are only as complete as the artifact data provided. Missing UTXO data, omitted redeem scripts, absent witness scripts, incomplete PSBT maps, descriptors without operational wallet context, and review packs without matching descriptor/PSBT/transaction artifacts can all reduce confidence.
234+
235+
Review-pack cross-artifact checks are intentionally limited. The tool compares available policy signals and input/output counts, but it does not prove descriptor-to-PSBT equivalence, transaction extraction from PSBT, key origin correctness, signer-set ownership, or wallet state.
207236

208237
## Technical Due Diligence Connection
209238

ROADMAP.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- [x] PSBT analysis from base64 input
88
- [x] script analysis from hex or a small ASM subset
99
- [x] descriptor parsing and policy hints with `miniscript`
10+
- [x] review-pack reports for descriptor, PSBT, transaction, script, policy, and notes directories
1011
- [x] report schema versioning
1112
- [x] JSON and Markdown reports
1213
- [x] risk warnings with human explanations
@@ -30,7 +31,7 @@
3031

3132
## Due Diligence Use Cases
3233

33-
- batch analysis for transaction review packs
34+
- richer batch analysis for transaction review packs
3435
- policy review for multisig and timelock setups
3536
- PSBT readiness checklist
3637
- executive PDF or Markdown due diligence reports
@@ -44,4 +45,5 @@
4445
- seed phrase handling
4546
- custody
4647
- broadcasting
48+
- review-pack network calls
4749
- consensus-level validation claims

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod analyzer;
22
pub mod report;
3+
pub mod review_pack;
34

45
#[cfg(feature = "ai")]
56
pub mod ai;

src/main.rs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
use std::path::PathBuf;
1+
use std::{fs, path::PathBuf};
22

33
use anyhow::{bail, Result};
44
use clap::{Parser, Subcommand, ValueEnum};
55

66
use btc_risk_lab::analyzer;
7-
use btc_risk_lab::report::{render_report, OutputFormat};
7+
use btc_risk_lab::report::{render_report, render_review_pack_report, OutputFormat};
88

99
#[derive(Debug, Parser)]
1010
#[command(author, version, about)]
@@ -77,6 +77,18 @@ enum Commands {
7777
format: CliFormat,
7878
},
7979

80+
/// Analyze a local directory containing descriptor, PSBT, transaction, script, policy, and notes artifacts.
81+
ReviewPack {
82+
#[arg(long, value_name = "DIR")]
83+
input: PathBuf,
84+
85+
#[arg(long, value_enum, default_value_t = CliFormat::Markdown)]
86+
format: CliFormat,
87+
88+
#[arg(long, value_name = "FILE")]
89+
output: Option<PathBuf>,
90+
},
91+
8092
/// Generate an optional executive summary from an existing technical JSON report.
8193
Summarize {
8294
#[arg(long)]
@@ -136,12 +148,30 @@ fn main() -> Result<()> {
136148
let report = analyzer::analyze_descriptor_input(&descriptor)?;
137149
println!("{}", render_report(&report, format.into())?);
138150
}
151+
Commands::ReviewPack {
152+
input,
153+
format,
154+
output,
155+
} => {
156+
let report = btc_risk_lab::review_pack::analyze_review_pack(&input)?;
157+
write_or_print(render_review_pack_report(&report, format.into())?, output)?;
158+
}
139159
Commands::Summarize { input, provider } => summarize(input, provider)?,
140160
}
141161

142162
Ok(())
143163
}
144164

165+
fn write_or_print(rendered: String, output: Option<PathBuf>) -> Result<()> {
166+
if let Some(output) = output {
167+
fs::write(&output, rendered)?;
168+
} else {
169+
println!("{rendered}");
170+
}
171+
172+
Ok(())
173+
}
174+
145175
#[cfg(feature = "fetch")]
146176
fn fetch_tx(txid: &str) -> Result<btc_risk_lab::analyzer::RiskReport> {
147177
let runtime = tokio::runtime::Builder::new_multi_thread()

src/report/mod.rs

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use anyhow::Result;
22

3-
use crate::analyzer::{ArtifactType, RiskReport};
3+
use crate::analyzer::{ArtifactType, RiskLevel, RiskReport};
4+
use crate::review_pack::ReviewPackReport;
45

56
#[derive(Clone, Copy, Debug)]
67
pub enum OutputFormat {
@@ -15,6 +16,16 @@ pub fn render_report(report: &RiskReport, format: OutputFormat) -> Result<String
1516
}
1617
}
1718

19+
pub fn render_review_pack_report(
20+
report: &ReviewPackReport,
21+
format: OutputFormat,
22+
) -> Result<String> {
23+
match format {
24+
OutputFormat::Json => Ok(serde_json::to_string_pretty(report)?),
25+
OutputFormat::Markdown => Ok(render_review_pack_markdown(report)),
26+
}
27+
}
28+
1829
fn render_markdown(report: &RiskReport) -> String {
1930
let mut markdown = String::new();
2031
markdown.push_str("# BTC Risk Lab Report\n\n");
@@ -130,6 +141,116 @@ fn push_outputs_table(markdown: &mut String, outputs: &[crate::analyzer::OutputA
130141
}
131142
}
132143

144+
fn render_review_pack_markdown(report: &ReviewPackReport) -> String {
145+
let mut markdown = String::new();
146+
markdown.push_str("# BTC Risk Lab Review Pack\n\n");
147+
markdown.push_str(&format!("- Schema: `{}`\n", report.schema_version));
148+
markdown.push_str(&format!(
149+
"- Consolidated risk: `{}`\n\n",
150+
risk_name(&report.consolidated_risk)
151+
));
152+
153+
markdown.push_str("## Artifacts Detected\n\n");
154+
if report.artifacts_detected.is_empty() {
155+
markdown.push_str("- No known review pack artifacts were detected.\n\n");
156+
} else {
157+
markdown.push_str("| Artifact | File |\n|---|---|\n");
158+
for artifact in &report.artifacts_detected {
159+
markdown.push_str(&format!(
160+
"| `{}` | `{}` |\n",
161+
artifact.artifact, artifact.file
162+
));
163+
}
164+
markdown.push('\n');
165+
}
166+
167+
markdown.push_str("## Per-Artifact Summary\n\n");
168+
if report.per_artifact_summary.is_empty() {
169+
markdown.push_str("- No artifact summaries available.\n\n");
170+
} else {
171+
for artifact in &report.per_artifact_summary {
172+
markdown.push_str(&format!(
173+
"### `{}` ({:?})\n\n",
174+
artifact.artifact, artifact.status
175+
));
176+
if let Some(risk) = &artifact.risk {
177+
markdown.push_str(&format!("- Risk: `{}`\n", risk_name(risk)));
178+
}
179+
for item in &artifact.summary {
180+
markdown.push_str(&format!("- {}: `{}`\n", item.label, item.value));
181+
}
182+
if !artifact.missing_data.is_empty() {
183+
markdown.push_str("- Missing data:\n");
184+
for item in &artifact.missing_data {
185+
markdown.push_str(&format!(" - {}\n", item));
186+
}
187+
}
188+
if !artifact.warnings.is_empty() {
189+
markdown.push_str("- Warnings:\n");
190+
for warning in &artifact.warnings {
191+
markdown.push_str(&format!(
192+
" - **{}** (`{:?}`, `{}`): {}\n",
193+
warning.title, warning.severity, warning.code, warning.explanation
194+
));
195+
}
196+
}
197+
markdown.push('\n');
198+
}
199+
}
200+
201+
if !report.warnings.is_empty() {
202+
markdown.push_str("## Warnings\n\n");
203+
for warning in &report.warnings {
204+
markdown.push_str(&format!(
205+
"- **{}** (`{:?}`, `{}`): {}\n",
206+
warning.title, warning.severity, warning.code, warning.explanation
207+
));
208+
}
209+
markdown.push('\n');
210+
}
211+
212+
if !report.missing_data.is_empty() {
213+
markdown.push_str("## Missing Data\n\n");
214+
for item in &report.missing_data {
215+
markdown.push_str(&format!("- {}\n", item));
216+
}
217+
markdown.push('\n');
218+
}
219+
220+
if !report.cross_artifact_findings.is_empty() {
221+
markdown.push_str("## Cross-Artifact Findings\n\n");
222+
for finding in &report.cross_artifact_findings {
223+
markdown.push_str(&format!(
224+
"- **{}** (`{:?}`, `{}`): {}\n",
225+
finding.title, finding.severity, finding.code, finding.explanation
226+
));
227+
}
228+
markdown.push('\n');
229+
}
230+
231+
markdown.push_str("## Review Questions\n\n");
232+
for question in &report.review_questions {
233+
markdown.push_str(&format!("- {}\n", question));
234+
}
235+
markdown.push('\n');
236+
237+
markdown.push_str("## Limitations\n\n");
238+
for limitation in &report.limitations {
239+
markdown.push_str(&format!("- {}\n", limitation));
240+
}
241+
242+
markdown
243+
}
244+
245+
fn risk_name(risk: &RiskLevel) -> &'static str {
246+
match risk {
247+
RiskLevel::Low => "low",
248+
RiskLevel::Medium => "medium",
249+
RiskLevel::High => "high",
250+
RiskLevel::Unknown => "unknown",
251+
}
252+
}
253+
133254
fn artifact_name(artifact_type: &ArtifactType) -> &'static str {
134255
match artifact_type {
135256
ArtifactType::Transaction => "transaction",

0 commit comments

Comments
 (0)