Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ All notable changes to vouch are documented here. Format follows

## [Unreleased]

### Added
- `vouch import <format> <path>` — conversation-export importers that parse
chat-json, markdown-vault, or memory-export dumps and file pending
proposals via the existing review gate. supports `--dry-run` for counts
without enqueuing and `--max-proposals` to cap a single run. imported
claims are de-duped against approved artifacts via propose-time
similarity (#431).
## [1.2.2] — 2026-07-07

### Packaging
Expand Down
78 changes: 78 additions & 0 deletions src/vouch/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from . import __version__, bundle, health, volunteer_context
from . import audit as audit_mod
from . import capture as capture_mod
from . import corpus_import as corpus_import_mod
from . import codex_rollout as codex_rollout_mod
from . import compile as compile_mod
from . import digest as digest_mod
Expand Down Expand Up @@ -2828,6 +2829,83 @@ def export(out_path: str) -> None:
)


@cli.command("import")
@click.argument(
"format",
type=click.Choice(list(corpus_import_mod.IMPORT_FORMATS)),
)
@click.argument("path", type=click.Path(exists=True))
@click.option(
"--dry-run",
is_flag=True,
help="Report would-propose counts without enqueuing proposals.",
)
@click.option(
"--max-proposals",
type=int,
default=None,
help="Cap proposals filed in one run (claims + pages combined).",
)
Comment on lines +2843 to +2848

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate --max-proposals against non-positive values.

type=int with default=None accepts 0 and negative values, which could cause confusing behavior depending on how run_import handles them. Using click.IntRange(min=1) rejects invalid values at the CLI layer with a clear error message, while still allowing None (no cap) as the default.

🛡️ Proposed fix
 `@click.option`(
     "--max-proposals",
-    type=int,
+    type=click.IntRange(min=1),
     default=None,
     help="Cap proposals filed in one run (claims + pages combined).",
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@click.option(
"--max-proposals",
type=int,
default=None,
help="Cap proposals filed in one run (claims + pages combined).",
)
`@click.option`(
"--max-proposals",
type=click.IntRange(min=1),
default=None,
help="Cap proposals filed in one run (claims + pages combined).",
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/cli.py` around lines 2053 - 2058, The --max-proposals option in the
click option definition currently accepts 0 and negative integers because it
uses plain type=int with default=None. Update the CLI validation in the option
decorator for the run_import-related command to use a positive integer
constraint such as click.IntRange(min=1), while keeping None as the default so
the cap remains optional. This change should be made in the click.option
declaration for --max-proposals so invalid values are rejected before run_import
is called.

@click.option("--json", "as_json", is_flag=True, help="Emit machine-readable summary.")
def import_cmd(
format: str,
path: str,
dry_run: bool,
max_proposals: int | None,
as_json: bool,
) -> None:
"""Import conversation exports as review-gated proposals (#431).

Formats: chat-json, markdown-vault, memory-export. Every item lands in
``.vouch/proposed/`` — nothing is auto-approved.
"""
store = _load_store()
try:
result = corpus_import_mod.run_import(
store,
format, # type: ignore[arg-type]
Path(path),
dry_run=dry_run,
max_proposals=max_proposals,
actor=_whoami(),
)
except corpus_import_mod.CorpusImportError as e:
raise click.ClickException(str(e)) from e

payload = {
"format": result.format,
"path": result.path,
"dry_run": result.dry_run,
"claims_proposed": result.claims_proposed,
"pages_proposed": result.pages_proposed,
"claims_skipped_dedup": result.claims_skipped_dedup,
"pages_skipped_dedup": result.pages_skipped_dedup,
"cap_hit": result.cap_hit,
"proposal_ids": result.proposal_ids,
"warnings": result.warnings,
}
if as_json:
_emit_json(payload)
return

mode = "dry-run" if dry_run else "import"
click.echo(
f"{mode}: {result.claims_proposed} claim(s), "
f"{result.pages_proposed} page(s) proposed"
)
if result.claims_skipped_dedup or result.pages_skipped_dedup:
click.echo(
f" skipped dedup: {result.claims_skipped_dedup} claim(s), "
f"{result.pages_skipped_dedup} page(s)"
)
if result.cap_hit:
click.echo(" cap hit: --max-proposals limit reached")
for pid in result.proposal_ids:
click.echo(f" + {pid}")
for w in result.warnings:
click.echo(f" warning: {w}", err=True)


@cli.command("export-check")
@click.argument("bundle_path", type=click.Path(exists=True, dir_okay=False))
def export_check_cmd(bundle_path: str) -> None:
Expand Down
Loading