Test - #571
Conversation
phase 3 of global vouch. `vouch hub init-personal` creates + registers a personal kb at ~/.local/share/vouch/personal (XDG_DATA_HOME honoured, VOUCH_PERSONAL_KB overrides). with its own config's personal.fallback_capture flag on (opt-in: one question on a terminal at `install-mcp --global`, or --personal-fallback / `vouch hub fallback on`), sessions in folders WITHOUT a project kb capture into it — origin folder stamped on every captured source — and recall reads it back from the same folders. the session banner announces the routing; a guard refusal (discovery landing on a personal kb from below, the hijack shape) never falls through to fallback. `vouch adopt`, run inside a project that now has its own kb, drains those captures home THROUGH the project's review gate: sources copy byte-identically (content-addressed ids survive), each live personal claim re-proposes against the copied source, receipts re-verify mechanically, and the project's own review config decides durability. idempotent; --retire archives the personal copies; both kbs log a kb.adopt event carrying the other side's id.
the "a folder without a kb never captures anywhere" wording gains its "by default" qualifier now that the opt-in personal fallback exists; both readmes explain the opt-in, the origin stamping, the banner announcement, and that adoption re-verifies receipts through the project kb's own review gate.
adversarial review of the phase-3 diff confirmed 12 findings (6 refuted). the blocker: `adopt --retire` archived personal claims that had only landed PENDING in the project, so rejecting or expiring the proposal left the knowledge live in neither kb with no unarchive path. retire now covers exactly the claims that became durable. the rest: - re-running adopt filed a duplicate pending proposal per pass under a human-only gate (`_already_durable` never saw the queue) — the guard now checks pending payload ids too, and --dry-run predicts against the project's real gate instead of assuming receipts auto-approve. - fallback recall reads the whole personal kb, which is what a catch-all is — but the injected digest called it "knowledge for this repo". the digest header, the prompt-hook block, the banner, the opt-in question and both readmes now say it is one store shared by every kb-less folder, so recall there can surface knowledge captured elsewhere. - session rollups filed by the fallback carried no origin: they now record the folder like captured sources do, and adopt reports the ones still pending in the personal kb instead of leaving them silent. - a stale personal registry row could shadow a live one and switch fallback off; live rows now win and init-personal retires rows pointing elsewhere. - OSError from the personal-kb bootstrap escaped as a traceback and made a successful --global install exit 1; both entry points now report a remedy, and the install warns without failing. - `vouch status` in a kb-less folder said only "run vouch init" while the hook plane was capturing to the personal kb; it now names it. - set_personal_fallback took the kb's cross-process lock like identity minting does, instead of a lock-free read-modify-write of config.yaml.
retire covers only claims that landed durable, adopt is idempotent against the pending queue too, dry-run predicts against the real gate, session rollups are reported rather than moved, and the personal kb is described as the one shared store it is.
feat(hub): personal catch-all kb, fallback capture, and vouch adopt
the per-prompt hook injected a block on every prompt, so a coding
command like "fix the failing test" spent its reply opener announcing a
memory search nobody asked for. three cases now, decided by a
deterministic llm-free classifier (no added latency):
chatter with no informative tokens ("ok thanks", "which one is better?")
injects nothing. retrieval ORs every query token, so those used to match
claims on "one" and inject noise on turns that wanted none — this half
is ported from the unmerged #518, rebased onto phase 3 so the two stop
fighting over hooks.py.
"do work" imperatives get a smaller background pack (3 items / 700
chars) and NO reply contract: no "From vouch memory:" opener, no
blockquote ritual, cite an id inline only where it was actually relied
on — and nothing at all when the kb has no match. recall can still
inform the work (conventions, architecture) without taking the turn
over.
lookups — questions, what/why/how, anything not imperative — keep the
full visible-recall behaviour including the honest "Nothing in vouch on
this." on a miss. silence there would be indistinguishable from vouch
being broken, which is the thing 1.5.0 set out to fix.
the imperative test reads past politeness lead-ins ("please fix", "ok
now refactor", "can you run"), and a prompt made only of discourse
tokens ("continue", "go ahead") counts as work, not a question. new kbs
get the gate on; existing kbs behave exactly as before until they add
retrieval.prompt_gate.
the first cut of the prompt gate enumerated "action verbs" and defaulted everything else to a loud "From vouch memory:" block. that fails open: action verbs are an unbounded class, so any verb not on the list (vendorize, dockerize, spin-up, knock-out, yarn) fell through to loud recall — the exact noise the gate exists to stop. so stop making the hook guess. when the gate is on it now hands the host model ONE conditional instruction and lets the model — which already reads the prompt — choose per turn: a QUESTION the items answer opens with "From vouch memory:" and quotes them; a TASK (fix/build/change/run anything, known verb or not) uses them silently as background with no banner, citing an id inline only where relied on; irrelevant items are ignored. on a miss the same judgment applies — "Nothing in vouch on this." for a question, silence for a task. chatter with no informative tokens still injects nothing. no per-turn model call, no latency — the decision rides in instruction text the model already processes, and it generalizes to any phrasing because it no longer depends on recognizing the verb. the legacy unconditional block is kept intact behind the gate flag, so existing KBs are byte-for-byte unchanged until they add retrieval.prompt_gate; new KBs get delegation via the starter config. compliance was MEASURED before shipping, not assumed — real claude -p across haiku / sonnet / opus, offline and against the real context-hook pipeline. coding tasks were never wrongly announced on any tier (the goal). questions are announced by every tier in the real pipeline; the only misses were sonnet under-announcing when a test harness fed a KB-claiming block into an empty dir with hooks off, which it correctly distrusted — resolved in-environment. tests/test_hooks.py pins the injected block; tests/test_prompt_gate_live.py is an opt-in live probe (skips without an authenticated claude cli).
…n a retrieval error two coderabbit findings on the prompt-gate pr, both real: the informative-token filter dropped every all-digit token, losing issue ids, error codes (404/500), and ports (8080) — exactly the terms a technical query needs. the len < 2 check already drops single-digit noise, so only tok.isdigit() had to go. _retrieve_body returned (None, "") on a build_context_pack exception, which both injection paths saw as a genuine empty result — so a broken index made the model falsely assert "Nothing in vouch on this." it now raises, and the hook dispatch catches any retrieval/render error and injects NOTHING, restoring the pre-gate fail-safe. a true empty search (no exception) still gets the honest miss message.
feat(hooks): prompt gate — let the model decide when recall is loud
the receiving side of vouchhub. inbound bundle knowledge no longer writes straight to the committed store: bundle.import_as_proposals files each inbound claim as a pending proposal via propose_claim, so nothing lands without passing this kb's own proposals.approve() -- the load-bearing invariant (roadmap step 10) upheld by design, not by surface removal. - import_as_proposals: sources/evidence registered as substrate (the inbox shape), claims filed as pending proposals; pages/entities/relations reported as deferred, never silently dropped. - provenance: the proposing actor is hub:<origin-kb> and each proposed claim carries an origin:<kb> tag that survives approval, so a reviewer and the durable claim both show which kb vouched. - kb instance id: a lazy per-kb identity in a .vouch/instance_id sidecar, stamped into the bundle manifest as kb_id; kept out of config and every exported subdir so identity never travels inside a bundle or collides with a receiver's config on import. - hub pull rewired to the gated path (no on-conflict -- a colliding claim is another proposal for the reviewer, not a destructive overwrite); new vouch import-proposals cli for file-based gated federation. tests: test_gated_import.py covers the gated lifecycle, provenance, conformance (only approve makes it durable), and instance id; the hub pull tests are rewired to assert proposals instead of committed writes. full suite green, mypy clean.
closes the second federation receive path that bypassed the review gate. sync_apply --as-proposals lands another kb's NEW claims as pending proposals (sources/evidence registered as substrate; pages/entities/relations/sessions/ decided reported as deferred), so nothing reaches the committed store without passing this kb's own proposals.approve() -- the roadmap step-10 invariant. the direct-write default is unchanged: like import-apply it stays the human tool for reconciling your own kbs. --as-proposals (roadmap 8.2) is the federation-safe path for accepting another kb's knowledge. the propose-inbound-claim + origin-tag provenance logic is extracted from bundle.import_as_proposals into shared bundle.propose_inbound_claim and inbound_claim_id, so both gated receive paths (bundle import and sync) stamp identical provenance. tests: gated sync files pending not committed, approves into a claim carrying the origin tag, and the cli --as-proposals flow. full suite green, mypy clean.
a claim imported through the gate carries an origin:<kb> tag; build_context_pack now reads it onto each returned item (ContextItem.origin) and lists every contributing kb in the pack's origins field, so a reader can see which knowledge came from another kb and which is local -- the read-time half of "every result names the kb that vouched for it" (roadmap step 10). locally-authored claims have origin=None and add no origins field. tests: a federated claim surfaces its origin on recall; a local claim does not. full suite green, mypy clean.
resolve the conflicts from the federation gate meeting the test branch's identity, prompt-gate and personal-kb work: - bundle.py: union both feature sets — the exclude filter + instance-id kb_id provenance (this branch) and identity-stripped config export + fenced_bundle_path + kb identity block (test). export now fences the dest, strips config identity, forwards exclude, and stamps both kb_id and kb; _yaml_dump drops its dead-code marker since test now calls it. - cli.py: the two branches each added a `hub` command group; a second @cli.group() silently shadowed the first. fold hub link/push/pull/status onto the single hub group so test's register/list/init-personal/fallback subcommands survive alongside them. - context.py: keep both result blocks — federated `origins` and the retrieval-honesty `retrieval` block. - jsonl_server.py: export handler fences the path (test) and forwards the exclude filter (this branch). also clear pre-existing ruff debt in the hub-client base commits so the now-computable merge passes ci (conflicts had blocked pull_request ci from running at all): line length, dead noqa directives, mutable class defaults via ClassVar, and a redundant session_split import surfaced by the merge.
…ield the gated-import work added ContextItem.origin (and the pack-level origins) but the generated json schemas were never regenerated — the drift was latent because the conflict blocked schema-check from running on the pr. regenerate context-item and context-pack from models.py so the drift gate passes.
feat(hub): gated federation — push/pull + receiving-side review gate
compile now drafts browsable, fully-grounded wiki pages that match the llm-wiki compiler's presentation without inheriting its failure modes. phase 1 — the page-draft prompt asks for typed markdown sections (what/why/ how, context/options/decision) plus a one-line summary, tags, and aliases, carried in page metadata. a citation-density guardrail drops any draft whose substantive sentences are mostly uncited, so richer prose cannot smuggle in assertions that trace to no claim. phase 2 — a new wiki_render module renders a derived index.md and a backlink map-of-content over approved pages (a regenerable view, never a gated write), exposed as `vouch render-wiki`. the wikilink resolver now matches title, slug, or alias, so a link to an existing page's alias no longer false-drops. phase 3 — optional two-phase compile (compile.two_phase) plans durable topics first, then drafts one focused page each. off by default; it doubles the llm calls in exchange for finer-grained pages. every write still lands as a pending proposal and every inline [claim: id] is verified against the store — the review gate and receipt fidelity are untouched; this only improves what the compiler drafts behind them.
feat(compile): structured pages, wiki index/moc, two-phase drafting
vouch has always gated *who approves* a write (proposals.approve) but never *whether the content is knowledge-shaped*. every ingestion path funnels through proposals._file_proposal, so one provenance-keyed admission predicate there raises the floor with no drift across surfaces. the gate is deterministic and receipt-safe — it only rejects verbatim payloads, never rewrites them, so byte-offset receipts stay intact. for claims it applies a structural floor (##+ headings, colon lead-ins, unbalanced code-spans/brackets) plus an optional confidence floor; for pages it rejects an uncited type:session/log page, a session diary rather than durable knowledge. it only blocks the passive auto-capture actors (vouch-capture, session-split, codex); a deliberate author's write stays advisory and reaches the review gate untouched. everything is configurable under the admission: block of config.yaml (enabled, min_confidence, reject_uncited_session_pages). auto-rejections are recorded (decided_by: vouch-admission) and reviewable with the new `vouch rejected` command — nothing is ever deleted. turning the page rule on by default deprecates the auto-captured session-page pipeline: capture.finalize, session_split renarrate, codex_rollout reingest, and the review banner become no-ops for these actors. the tests that asserted those pages land pending are updated to the auto-reject contract; deleting the dead machinery is a follow-up. resolve_pending_receipt_claim now skips a non-pending proposal so a gate-rejected fragment can no longer crash capture_answer's auto-approve loop and take the good claims beside it with it.
feat(admission): gate knowledge-shaped garbage at the proposal funnel
the structural floor caught ##-headings but not the emoji/bold heading convention — "✨ **What's new**", "**Summary**" — because the heading rule only matches ##-prefixed lines and ** is deliberately left unchecked (it would strand **kwargs). a receipt-verified span like that was being laundered straight into approved knowledge by the auto-capture path. add a claim rung that rejects a span whose entire body, after stripping leading emoji/symbol decoration, is a single closed emphasis run (**...**, *...*, _..._). high precision by construction: a claim that merely contains inline emphasis keeps prose outside the run, and a lone opening **kwargs has no closing delimiter, so both still admit.
the admission gate answers "is this knowledge-shaped?" structurally at the funnel. this adds the softer "is it worth remembering?" as a separate, advisory tier that never runs in the funnel and never mutates a payload, so determinism and byte-offset receipts stay intact. worthiness.py exposes a pluggable Scorer protocol and a default HeuristicScorer — fully local, no llm. it blends cheap deterministic signals (question, agent-directive imperative, leading deixis, has-a-verb, entity presence, length band) with a novelty check against the approved kb: a claim that near-duplicates an existing one (fts lookup + jaccard overlap) carries little marginal worth. the scorer emits a score in [0,1] plus the dominant reason. config lives in a top-level worthiness: block (scorer/min_score/action/ apply_to), parsed fail-soft with a yaml-1.1 guard so `scorer: off` disables scoring rather than parsing as boolean false. the llm backend and the review-queue wiring are deliberate follow-ups; this lands the deterministic engine and its tests.
fix(admission): reject emphasis-wrapped heading labels
feat(worthiness): tier-2 advisory claim-worthiness scoring
the pending queue could approve only one proposal at a time; clearing a review backlog meant opening and approving each in turn. selection checkboxes already existed but drove only page merges. show a selection checkbox on every approvable row, add a "select all" toggle and an "approve N selected" action that approves the checked rows in one pass — client-side fan-out per project, the same shape as the existing clear (reject-all) path, since there is no bulk-approve endpoint. merge stays pages-only off the same selection set.
feat(webapp): batch-approve pending proposals from the review queue
open contributors kept needing the lane rules from three scattered docs. contributing.md now states the two pr shapes the gates enforce: engine lane = one new file under contrib/strategies/ against koth-ladder, sandboxed, scored, never auto-merged, human review for benchmark-keyed logic; kit lane = kit.yaml only, auto-merge on dethrone, low ceiling by construction. includes the local ci-exact practice command and the reproducibility and payout pointers.
workflow lint only runs on prs that touch workflows, so the koth gates' pull_request_target findings never surfaced until the engine-gate registration pr woke the linter. both gates now carry the ignore annotation with the justification inline (base-branch code only, pr content handled as data, read-only tokens), and the scorecard comment script declares its literal backticks for shellcheck.
workflow lint only runs on prs that touch workflows, so the koth gates' pull_request_target findings never surfaced until the engine-gate registration pr woke the linter. both gates now carry the ignore annotation with the justification inline (base-branch code only, pr content handled as data, read-only tokens), and the scorecard comment script declares its literal backticks for shellcheck.
feat(competition): provenance-rank engine-lane submission
Feat/koth engine lane
pin checkout and setup-python to immutable commit shas (mutable tags in a pull_request_target workflow are a supply-chain surface), and classify a delete-only strategy pr as normal instead of engine - the filename shape matched but there is nothing to fetch at the head sha, so the gate failed messy instead of passing through.
pin checkout and setup-python to immutable commit shas (mutable tags in a pull_request_target workflow are a supply-chain surface), and classify a delete-only strategy pr as normal instead of engine - the filename shape matched but there is nothing to fetch at the head sha, so the gate failed messy instead of passing through.
Koth ladder
WalkthroughThis PR adds VouchBench competition infrastructure, retrieval strategies, admission and worthiness checks, personal-KB fallback and adoption, gated federation, session enrichment, dead-reference cleanup, wiki rendering, documentation, and related webapp routes and controls. ChangesCompetition and benchmark infrastructure
Capture, admission, and federation
Compilation and webapp
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
the engine-lane winner (pr #567) moves in-package as vouch.strategies.provenance and becomes what challengers must beat: contrib/strategies/baseline.py now delegates to it instead of returning identity order. new KBs get it as the final reorder stage via the starter config (retrieval.strategy); existing KBs keep byte-identical ordering until they add the key, and strategy: null opts out. the bench is unaffected - its throwaway KBs write their own config, so kits and strategies keep being measured explicitly. rerank-isolation tests opt out of the strategy stage, since the champion is final-say and would re-sort the asserted window.
feat(retrieval): ship the provenance champion as the retrieval default
the enrich pipeline already compiles captured conversations into type=session pages (summary, typed subjects, activity), but the console had no surface that showed them — ditto's webapp makes its compiled memories browsable and vouch's stayed invisible in .vouch/pages. adds a /sessions card view: newest-first cards with the enrich summary, subject chips, and created date, filterable client-side, each card deep-linking into the existing /browse page drawer. fans out over all scoped projects like the other list views and degrades to the usual unavailable/empty states when kb.list_pages is not advertised.
feat(webapp): sessions view for compiled conversation history
There was a problem hiding this comment.
Actionable comments posted: 11
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (17)
src/vouch/bundle.py-883-903 (1)
883-903: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winInbound lifecycle links are dropped silently.
claim.supersedes,contradicts, andsuperseded_byare not forwarded topropose_claim, so an inbound claim that retires another arrives with its lifecycle edges stripped. The docstring only calls out pages/entities/relations as deferred — worth mentioning these too (or reporting them alongsidedeferred) so a reviewer doesn't read the import as lossless.🤖 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/bundle.py` around lines 883 - 903, Update the inbound claim import flow around Claim.model_validate and _proposals.propose_claim to forward claim.supersedes, claim.contradicts, and claim.superseded_by when creating the proposal. Also update the surrounding docstring or deferred-reporting behavior to explicitly identify these lifecycle links as deferred or otherwise reported, without implying the import is lossless.src/vouch/worthiness.py-210-217 (1)
210-217: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
min_score: 0is silently rewritten to 0.4.
_as_float(...) or DEFAULT_MIN_SCOREtreats a valid0.0as absent, so an operator disabling the threshold gets the default back instead. Distinguish "missing" from "zero".🐛 Proposed fix
+ min_score = _as_float(raw.get("min_score")) return WorthinessConfig( # YAML 1.1 parses a bare ``off`` as boolean False — coerce it back so # ``scorer: off`` disables scoring rather than becoming the string "False". scorer=_normalize_scorer(raw.get("scorer", DEFAULT_SCORER)), - min_score=_as_float(raw.get("min_score")) or DEFAULT_MIN_SCORE, + min_score=DEFAULT_MIN_SCORE if min_score is None else min_score, action=str(raw.get("action", DEFAULT_ACTION)), apply_to=apply_to, )
admission.load_config(Line 107) carries the same pattern; it is harmless today only because its default is0.0— worth tightening there as well so the trap doesn't fire the day that default changes.🤖 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/worthiness.py` around lines 210 - 217, Update the min_score assignment in WorthinessConfig construction to distinguish a missing value from a valid 0.0, preserving zero instead of falling back to DEFAULT_MIN_SCORE. Also tighten the equivalent defaulting logic in admission.load_config so its parsed zero value is retained independently of the current default.src/vouch/memory_contract.py-113-125 (1)
113-125: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winUnguarded source read can abort a whole search.
_evidence_oftolerates a missing Evidence record, butself.store.read_source_content(ev.source_id)is unprotected — a pruned/absent source blob raises out of_hitand takes downsearch_memories,fetch_by_id, andsearch_in_subject. Also note the source bytes are re-read for every evidence of every hit.🛡️ Proposed guard
+ def _receipt_backed(self, claim: Claim) -> bool: + for ev in self._evidence_of(claim): + try: + content = self.store.read_source_content(ev.source_id) + except (ArtifactNotFoundError, OSError): + continue + if verify_receipt(ev, content).verified: + return True + return Falsethen
receipt_backed=self._receipt_backed(claim).🤖 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/memory_contract.py` around lines 113 - 125, Update the MemoryHit construction in _hit to use a dedicated _receipt_backed(claim) helper. Implement that helper to safely handle missing or pruned source content while checking each evidence receipt, treating unreadable sources as unverified, and avoid rereading the same source blob when multiple evidence records reference it.tests/test_hub_client.py-134-142 (1)
134-142: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winClose the server socket after
shutdown().
shutdown()only stops the serve loop; withoutserver_close()the listening socket stays open for the life of the test session, leaking a file descriptor per test using this fixture.🛡️ Proposed fix
yield f"http://127.0.0.1:{srv.server_port}" srv.shutdown() + srv.server_close() + t.join(timeout=5)🤖 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 `@tests/test_hub_client.py` around lines 134 - 142, Update the fake_hub fixture teardown to call server_close() after shutdown(), ensuring the ThreadingHTTPServer listening socket is released for every test using the fixture.src/vouch/hub.py-465-473 (1)
465-473: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Path(entry.path)here skips theexpanduser()thatpersonal_entriesapplies.A registry row whose
pathcontains~passes the live-row filter on line 354 but then fails(root / KB_DIRNAME).is_dir()here, so fallback silently reports off. Same asymmetry exists in the CLI'shub fallback/adoptresolution ofentry.path, which is why fixing it at the source is preferable.🛡️ Proposed fix
- root = Path(entry.path) + root = Path(entry.path).expanduser()🤖 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/hub.py` around lines 465 - 473, Update the fallback resolution around personal_entry and the CLI hub fallback/adopt entry-path resolution to expand user-home syntax before constructing Path objects. Reuse the same expanduser behavior applied by personal_entries so registry paths containing ~ resolve consistently and existing directory and fallback checks operate on the expanded path..github/scripts/update_leaderboard.py-55-61 (1)
55-61: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
max()over an empty sequence raises when the ledger has no table row yet.
next_row_numberalready handles the no-table case (returns 0), butappend_rowthen callsmax()on an empty generator and dies with an unhandledValueErrortraceback — andmainnever returns 1, so the documented "exit code 1 = error" contract isn't met either. Appending at EOF is the natural fallback.🛡️ Proposed fix
lines = text.splitlines(keepends=True) - last_row = max( - i for i, line in enumerate(lines) if line.startswith("| ") - ) - lines.insert(last_row + 1, row + "\n") + row_indices = [i for i, line in enumerate(lines) if line.startswith("| ")] + insert_at = row_indices[-1] + 1 if row_indices else len(lines) + lines.insert(insert_at, row + "\n") ledger.write_text("".join(lines), encoding="utf-8")🤖 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 @.github/scripts/update_leaderboard.py around lines 55 - 61, Update append_row to handle ledgers with no lines starting with "| " by appending the new row at EOF instead of calling max() on an empty generator; preserve the existing insertion-after-last-row behavior when a table row exists. Ensure main converts append_row failures into the documented exit code 1 rather than allowing an unhandled ValueError traceback.src/vouch/cli.py-5440-5443 (1)
5440-5443: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winA malformed
--seedsvalue raises a bareValueErrortraceback.
int(s)on e.g.--seeds 1,xescapes as an unhandled exception rather than a click usage error.🛡️ Proposed fix
- seed_list = ( - [int(s) for s in seeds.replace(" ", "").split(",") if s] - if seeds else [seed] - ) + if seeds: + try: + seed_list = [int(s) for s in seeds.replace(" ", "").split(",") if s] + except ValueError as e: + raise click.UsageError(f"--seeds must be a comma-separated integer list: {e}") from e + else: + seed_list = [seed]🤖 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 5440 - 5443, Update the seed_list parsing in the CLI command to catch invalid integer values from --seeds and raise a Click usage error instead of allowing ValueError to escape. Preserve valid comma-separated seed parsing and the existing fallback to seed when --seeds is omitted.tests/test_dead_claim_refs.py-134-160 (1)
134-160: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAssert the
idecho in the JSONL envelope.Both tests check
ok/result/errorbut not that the response echoes the requestid, which is part of the envelope contract these tests exist to pin.As per coding guidelines, "For each new
kb.*method, add a test that asserts the JSONL envelope shape:{id, ok, result}on success and{id, ok: false, error}on failure."💚 Proposed addition
resp = handle_request({"id": "r1", "method": "kb.approve", "params": {"proposal_id": pr.id}}) + assert resp["id"] == "r1" assert not resp["ok"] assert resp["error"]["code"] == "dead_claim_refs"🤖 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 `@tests/test_dead_claim_refs.py` around lines 134 - 160, Update test_jsonl_approve_dead_refs_error_code and test_jsonl_wipe_dead_refs to assert that every handle_request response echoes the corresponding request id: r1 for the first response and r2 for the second. Preserve the existing success, error, and result assertions.Source: Coding guidelines
README.md-132-135 (1)
132-135: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winBlank line inside blockquote (MD028).
The blank line between the two
>-prefixed paragraphs can render inconsistently across Markdown renderers. Prefix the blank separator line with>(or merge into one blockquote) to keep rendering consistent.🤖 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 `@README.md` around lines 132 - 135, Update the blockquote containing the “Prefer one install for every project?” and “Optionally, a personal catch-all KB.” paragraphs by prefixing the blank separator line with `>`; preserve both paragraphs and their content unchanged.Source: Linters/SAST tools
CHANGELOG.md-9-47 (1)
9-47: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winMissing changelog entry for gated federation import / provenance surfacing.
The PR objectives and
src/vouch/models.py(ContextItem.origin) plustests/test_gated_import.pypoint to a shipped "federated provenance surfaced during retrieval" feature (a claim imported from another KB reports its origin at recall time viapack["origins"]/item["origin"]). This doesn't appear as its own bullet in the shown[Unreleased]section — the closest entry ("shipped ranking champion") is about a ranking algorithm, not the import/origin-tracking feature. As per coding guidelines, "When adding a user-visible feature, update CHANGELOG.md under[Unreleased]in the same PR; do not omit the changelog entry."🤖 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 `@CHANGELOG.md` around lines 9 - 47, Add a separate [Unreleased] changelog bullet documenting gated federation import and provenance surfacing during retrieval. Mention that imported claims expose their originating knowledge base through retrieval results, including pack["origins"] and item["origin"], and reference the relevant user-visible behavior without conflating it with the ranking entry.Source: Coding guidelines
.github/workflows/koth-ledger.yml-58-61 (1)
58-61: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winAvoid swallowing PR comment fetch failures
pull-requests: readis the right job permission for reading pull request comment data via the Issues comments endpoint. The concern is the|| continue: a permission issue, rate limit, network error, or malformed API response can skip the entire PR’s comment parsing per loop iteration while the workflow still proceeds to “sweep done”. Treat API non-2xx/JSON failures as errors and only skip due to an empty/no-match result set, e.g. by returning a distinct exit code for that case.🤖 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 @.github/workflows/koth-ledger.yml around lines 58 - 61, Update the PR comment fetch loop around the gh api invocation so permission, rate-limit, network, non-2xx, and malformed-response failures stop the workflow instead of being swallowed by || continue. Preserve skipping only when the query produces no matching GitHub Actions bot comments, using a distinct no-match status and handling it separately from API errors before proceeding to “sweep done”.src/vouch/lifecycle.py-263-291 (1)
263-291: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winscan inline markers independently of frontmatter claims.
Dead ids are discovered only from
page.claims/payload["claims"]. A stale[claim: missing-id]marker absent from those lists remains unchanged, contradicting the cleanup contract and leaving an orphaned reference behind.🤖 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/lifecycle.py` around lines 263 - 291, Update the dead-claim cleanup flow around the page and proposal processing to scan inline claim markers in each body independently of frontmatter claims or payload["claims"]. Merge any marker IDs whose claim paths do not exist into the dead set, then remove them with strip_claim_markers and persist/index the changed page or proposal while preserving dry-run behavior.src/vouch/retrieval_events.py-164-165 (1)
164-165: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winreturn no records for
limit=0.
out[-0:]is equivalent toout[0:], so this returns every event rather than the newest zero events.proposed fix
- if limit is not None and limit >= 0: + if limit == 0: + return [] + if limit is not None and limit > 0: return out[-limit:]🤖 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/retrieval_events.py` around lines 164 - 165, Update the limit handling in the retrieval function around the `if limit is not None and limit >= 0` condition so `limit=0` returns an empty result instead of slicing with `out[-0:]`; preserve the existing newest-record behavior for positive limits and current behavior when the limit is unset.src/vouch/enrich.py-99-102 (1)
99-102: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
UnicodeDecodeErrorescapes this loader, breakingenrich_session's "never raises" contract.
read_text(encoding="utf-8")on a non-utf8config.yamlraisesUnicodeDecodeError, which is not caught here and propagates throughenrich_sessionintosession_split.summarize→capture.finalize, losing the session rollup.capture.load_configalready guards against this.🛡️ Proposed fix
- except (OSError, yaml.YAMLError): + except (OSError, UnicodeDecodeError, yaml.YAMLError): return EnrichConfig()🤖 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/enrich.py` around lines 99 - 102, Update the config-loading try/except in the enrich configuration loader to catch UnicodeDecodeError alongside OSError and yaml.YAMLError, returning EnrichConfig() for invalid UTF-8 input. Preserve the existing behavior for valid configurations and ensure enrich_session retains its never-raises contract.src/vouch/enrich.py-256-257 (1)
256-257: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
updatesis capped bymax_subjects.
capture.enrich.max_subjects: 1would silently drop all but the first detected update, and updates drive supersession of durable claims — an unrelated knob shouldn't govern that. Use a dedicated bound.🐛 Proposed fix
+MAX_UPDATES = 10 + ... - if len(updates) == max_subjects: + if len(updates) >= MAX_UPDATES: break🤖 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/enrich.py` around lines 256 - 257, Replace the max_subjects-based stopping condition in the update collection loop with a dedicated bound for detected updates, so capture.enrich.max_subjects does not truncate updates or durable-claim supersession; define or reuse the appropriate update-limit configuration near the surrounding enrichment logic and preserve the existing subject limit behavior separately.src/vouch/strategy.py-276-290 (1)
276-290: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winThe
open-mode check doesn't cover filesystem mutations that never open a file.
os.remove,os.unlink,os.rename,os.replace,os.rmdir,os.mkdir,os.truncate,os.chmod, andos.symlink/os.linkeach raise their own audit events, so a submission can delete or rename files on the runner while the docstring and error message both claim "filesystem writes are blocked". Cheap to close by adding them to the blocked set (defence in depth, not a boundary — the runner remains the trust root).🔒️ Proposed change
_BLOCKED_EXACT = frozenset({ "os.system", "os.exec", "os.fork", "os.forkpty", "os.posix_spawn", "os.spawn", + "os.remove", + "os.rename", + "os.rmdir", + "os.mkdir", + "os.truncate", + "os.chmod", + "os.symlink", + "os.link", "subprocess.Popen",🤖 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/strategy.py` around lines 276 - 290, Extend the audit-event blocking configuration used by _hook to include filesystem mutation events for remove/unlink, rename/replace, rmdir/mkdir, truncate, chmod, symlink, and link. Keep these events covered by the existing blocked_exact check so they raise the same sandbox PermissionError and preserve the current open-mode and flag checks.tests/test_codex_rollout.py-195-211 (1)
195-211: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winStale test name: no longer matches its own assertions.
test_reingest_grown_session_updates_pending_in_placenow asserts the exact opposite — the grown turn returns"already-ingested"and the body is not refreshed. The docstring-style comment above it already explains this reversal; only the function name is out of date.✏️ Suggested rename
-def test_reingest_grown_session_updates_pending_in_place( +def test_reingest_grown_turn_is_noop_once_admission_rejects( store: KBStore, tmp_path: Path ) -> None:🤖 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 `@tests/test_codex_rollout.py` around lines 195 - 211, Rename the test function test_reingest_grown_session_updates_pending_in_place to reflect that re-ingesting the grown session is skipped as already ingested and does not refresh the rejected proposal body. Leave the existing assertions and explanatory comment unchanged.
🧹 Nitpick comments (26)
src/vouch/worthiness.py (1)
33-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate actor set — import
admission.AUTO_CAPTURE_ACTORS.This is byte-identical to
admission.py:50-52; two copies will drift the first time a new capture actor is added on one side only.♻️ Proposed refactor
-AUTO_CAPTURE_ACTORS: frozenset[str] = frozenset({"vouch-capture", "session-split", "codex"}) +from .admission import AUTO_CAPTURE_ACTORS # tier 1 owns the firehose list🤖 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/worthiness.py` around lines 33 - 36, Remove the duplicate AUTO_CAPTURE_ACTORS definition from worthiness.py and import and reuse admission.AUTO_CAPTURE_ACTORS instead. Update references in the worthiness logic to resolve through the imported admission symbol, preserving the existing actor classification behavior..github/scripts/koth_score.py (1)
46-50: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueWall-clock budget is 24 × (sessions−1) × 2s ≈ 4 minutes of pure sleep.
With
N_SEEDS = 12and two arms,score()invokesbench.run24 times, each sleepingSESSION_GAP_SECONDSbetween session ingests. Worth confirming the ladder job's timeout has headroom, or consider threading the gap only where recency actually matters.🤖 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 @.github/scripts/koth_score.py around lines 46 - 50, Review the session-ingest timing in score() and bench.run: with N_SEEDS and both arms, the current SESSION_GAP_SECONDS produces roughly four minutes of cumulative wall-clock sleep. Confirm the ladder job timeout has sufficient headroom; if not, reduce or eliminate the gap for paths where recency ordering is not required while preserving it where timestamps must be spread.tests/test_strategy.py (2)
92-103: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse the
tmp_pathfixture here like the neighbouring tests.
tempfile.TemporaryDirectoryduplicates what pytest already provides and makes this the only test in the file with a different temp strategy.♻️ Proposed change
-def test_sandbox_only_returns_known_ids() -> None: +def test_sandbox_only_returns_known_ids(tmp_path: Path) -> None: @@ - # the child filters to valid ids before returning. - import tempfile - - with tempfile.TemporaryDirectory() as d: - path = _write(Path(d), body) - assert run_sandboxed(path, "q", CANDS, limit=10) == ["a", "c"] + # the child filters to valid ids before returning. + assert run_sandboxed(_write(tmp_path, body), "q", CANDS, limit=10) == ["a", "c"]🤖 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 `@tests/test_strategy.py` around lines 92 - 103, Update test_sandbox_only_returns_known_ids to use pytest’s tmp_path fixture instead of tempfile.TemporaryDirectory, passing the fixture’s directory to _write and removing the temporary-directory import and context manager while preserving the existing assertion.
211-224: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winString-replace config edit is brittle.
text.replace('strategy: vouch.strategies.provenance', 'strategy: null')silently no-ops if the starter config ever quotes or re-orders that key, and the assertion would then pass for the wrong reason. Prefer parsing the YAML, settingretrieval.strategy = None, and dumping it back.🤖 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 `@tests/test_strategy.py` around lines 211 - 224, The test’s configuration update uses a brittle string replacement that can silently fail. In test_configured_strategy_defaults_and_opt_out, parse store.config_path as YAML, set the retrieval.strategy value to None, and dump the modified configuration back before asserting _configured_strategy(store) is None.tests/test_retrieval_backend.py (1)
478-484: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRecency and pages_first tests don't opt out of the shipped strategy.
_set_rerankdisablesretrieval.strategyprecisely because the shipped champion is final-say, but_set_pages_firstand_set_recencyleave it on — so these ordering assertions also depend on the provenance blend (and on the strategy pool over-fetch) not inverting the boost. Mirroring the opt-out here keeps each stage's test isolated to that stage.♻️ Proposed change
cfg.setdefault("retrieval", {})["pages_first"] = pf_cfg + # isolate this stage: the shipped champion strategy is final-say. + cfg["retrieval"]["strategy"] = None store.config_path.write_text(yaml.safe_dump(cfg))🤖 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 `@tests/test_retrieval_backend.py` around lines 478 - 484, Update _set_pages_first and _set_recency to disable retrieval.strategy when configuring their respective test settings, matching _set_rerank. Keep each helper’s existing pages_first or recency configuration intact so ordering assertions isolate the targeted stage.src/vouch/memory_contract.py (2)
107-111: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value
search_in_subjectcan under-return silently.Filtering a
max(limit * 5, 25)pool by subject means a subject with many low-ranked memories returns fewer thanlimiteven though more exist. Worth documenting the approximation (or looping until the pool is exhausted) so a contract-scored run isn't penalised for a harness artifact.🤖 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/memory_contract.py` around lines 107 - 111, Update search_in_subject to avoid silently under-returning: continue fetching and filtering broader search results until limit subject matches are found or the search pool is exhausted. Preserve ranking order and the existing limit behavior, and document the approximation if the underlying search API cannot support reliable pagination.
41-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win"Not a live claim" is now expressed three ways. The root cause is that the retracted/retired status set has no shared public home, so each new consumer either copies the literal or reaches into a retrieval-module private.
src/vouch/memory_contract.py#L41-L45: drop the local_RETRACTEDliteral and consume the shared constant instead.src/vouch/health.py#L84-L90: replace the localfrom .context import _RETRACTED_CLAIM_STATUSESwith an import of that shared public constant (e.g. exported frommodels.py), so health no longer depends on a private retrieval symbol.🤖 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/memory_contract.py` around lines 41 - 45, Define or reuse one shared public constant for retracted claim statuses, such as in models.py, and update src/vouch/memory_contract.py lines 41-45 to consume it instead of defining _RETRACTED. Update src/vouch/health.py lines 84-90 to import that public constant rather than the private context symbol _RETRACTED_CLAIM_STATUSES; preserve existing status-check behavior at both sites.contrib/strategies/provenance_rank.py (1)
32-40: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueDocstring overstates the hearsay guard.
With
re.IGNORECASE,[a-z][a-z0-9-]*matches any leading word — pronouns included — so"i told bob his key rotated"is demoted as hearsay despite being first-hand. Either tighten the pattern (e.g. exclude first-person pronouns) or drop the "leading name-like token" claim.🤖 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 `@contrib/strategies/provenance_rank.py` around lines 32 - 40, Update the _HEARSAY pattern or its accompanying documentation so the implementation matches the stated first-hand safeguard: exclude first-person pronouns from the leading token pattern, preserving detection of named third-party speech while leaving phrases such as “i told bob his key rotated” untouched.src/vouch/health.py (1)
61-61: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
status()now scans claims and evidence twice.
receipt_coverage(store)walks every claim and evidence YAML, and lines 62/67 walk them again for the counts. For a "quick, no deep checks" summary consider computing the lists once and passing them in (or deriving the counts from the coverage pass).♻️ Sketch
- "receipt_coverage": receipt_coverage(store), - "claims": len(store.list_claims()), + "receipt_coverage": coverage, + "claims": len(claims),with
claims = store.list_claims()/evidence = store.list_evidence()hoisted andreceipt_coverageaccepting them.🤖 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/health.py` at line 61, Update status() and receipt_coverage() to avoid rescanning claims and evidence: hoist store.list_claims() and store.list_evidence() into status(), pass the resulting collections into receipt_coverage(), and derive the existing counts from those same collections. Preserve the current coverage and summary behavior while ensuring each YAML listing occurs only once.src/vouch/hooks.py (1)
289-370: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueGated path searches on stripped tokens while legacy searches the raw prompt.
_gated_injectionqueries" ".join(_informative_tokens(prompt))but_legacy_injectionstill passesprompt, so the two modes can return different item sets for the same turn. Intentional per the docstring, but worth a note in the module docstring (or applying the token filter to both) so a future comparison of gated vs legacy recall isn't confounded.🤖 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/hooks.py` around lines 289 - 370, The gated and legacy injection paths use different retrieval queries, with _gated_injection filtering prompt tokens while _legacy_injection searches the raw prompt. Apply _informative_tokens consistently to both paths, or document this intentional difference in the module docstring; preserve each path’s existing injection behavior and ensure comparisons are not misleading.src/vouch/context.py (1)
210-263: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winEach optional stage re-reads and re-parses
config.yaml.
_configured_pages_firstjoins_configured_recency,_configured_strategy(called twice per pack), and_configured_backend— onebuild_context_packnow does five-plus reads/parses of the same file on every query path. Consider loading the config once per call and threading the parsed mapping into the stage resolvers.🤖 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/context.py` around lines 210 - 263, Update build_context_pack and the optional-stage resolvers, including _configured_pages_first, _configured_recency, _configured_strategy, and _configured_backend, to load and parse config.yaml once per context-pack call. Thread the parsed configuration mapping through each resolver, including both _configured_strategy calls, while preserving existing defaults and behavior for invalid or missing configuration..github/scripts/score_strategy.py (1)
39-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider surfacing
SandboxProxyfailure counts in the report.
SandboxProxy.rankswallows sandbox failures and returns[](no reordering) while incrementingfailures. If the champion's sandbox child fails for environmental reasons, its scores silently degrade to raw backend order and the verdict can read "dethroned" for the wrong reason. Recordingproxy.failuresper arm in the JSON makes that visible to the human reviewer.♻️ Proposed change
-def score(path: str, seeds: list[int]) -> list[float]: +def score(path: str, seeds: list[int]) -> tuple[list[float], int]: proxy = SandboxProxy(path) - return [ + scores = [ bench.run(s, strategy=proxy, session_gap_seconds=SESSION_GAP_SECONDS)[ "composite" ] for s in seeds ] + return scores, proxy.failureswith
main()unpacking both and adding"sandbox_failures": {"champion": ..., "challenger": ...}toreport.🤖 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 @.github/scripts/score_strategy.py around lines 39 - 46, Update score to return both the computed composite scores and the associated SandboxProxy failure count, then have main() unpack these values for each arm and add a sandbox_failures object to report with champion and challenger counts. Preserve the existing scoring behavior while making failures visible in the generated JSON.tests/test_hooks.py (1)
287-301: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConfig-mutation helpers are near-duplicates.
Three helpers plus the inline block at lines 443-447 each re-import
yamland repeat the load/mutate/dump dance. A single_patch_retrieval(store, **keys)helper (module-levelimport yaml) would collapse them.Also applies to: 340-347, 443-447
🤖 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 `@tests/test_hooks.py` around lines 287 - 301, The config mutation helpers and inline retrieval update duplicate YAML load/write logic. Add a module-level yaml import and introduce a shared _patch_retrieval(store, **keys) helper that loads the configuration, updates retrieval with the supplied keys, and writes it back; refactor _enable_gate, _disable_gate, the helper around lines 340-347, and the inline block around lines 443-447 to use it while preserving their existing mutations..github/scripts/update_leaderboard.py (1)
32-42: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value
format_rowwill raiseKeyErroron a report missingchallenger/mean_diff.
lane/dateare read defensively but the two numeric fields aren't, so a malformed scorecard produces a traceback in CI rather than the documented error exit. Low priority given both scorers emit the same shape.🤖 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 @.github/scripts/update_leaderboard.py around lines 32 - 42, Update format_row to read challenger.mean and mean_diff defensively, matching the existing lane/date handling for malformed reports. Ensure missing numeric fields follow the script’s documented error-exit behavior instead of raising an uncaught KeyError, while preserving normal formatting for valid scorecards.tests/test_update_leaderboard.py (1)
44-62: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider covering the table-less ledger path.
next_row_numberis tested for"no table yet", butappend_rowisn't — which is exactly the input that currently raises frommax()in.github/scripts/update_leaderboard.py(flagged there). A test on that input would pin the fallback behaviour.🤖 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 `@tests/test_update_leaderboard.py` around lines 44 - 62, Add a test for append_row using a ledger with no table yet, covering the fallback row-number behavior that avoids the max() failure in next_row_number. Assert the call succeeds and verify the generated row is written with the expected initial numbering and placement.tests/test_capture.py (1)
1019-1022: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThis assertion can pass vacuously.
If the
sources/*/meta.yamlglob doesn't match the actual on-disk layout, the left operand is always true and the fallback-off guarantee is never checked. Assert the invariant directly instead.♻️ Proposed change
- assert not list((personal.kb_dir / "sources").glob("*/meta.yaml")) or all( - "origin_path" not in s.metadata for s in personal.list_sources() - ) + assert all("origin_path" not in s.metadata for s in personal.list_sources())🤖 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 `@tests/test_capture.py` around lines 1019 - 1022, Update the assertions in the capture test to validate the fallback-off invariant directly, rather than conditionally skipping it when the sources glob is empty. Remove the vacuous filesystem-glob condition and assert that every source returned by personal.list_sources() lacks "origin_path" in its metadata, while preserving the existing empty output assertion.src/vouch/cli.py (1)
1635-1666: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDetecting the dead-ref case by substring-matching
check_approvable's message is brittle.
proposals.DeadClaimRefsErroralready exists as the typed signal for exactly this case (seesrc/vouch/proposals.pylines 41-59). Keying the branch on"references unknown claim" in whymeans any reword of that validator message silently turns the strip-and-approve path back into a hard abort. Consider havingcheck_approvablereturn a structured reason (or callmissing_claim_refsunconditionally for page proposals) instead of parsing prose.🤖 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 1635 - 1666, The dead-reference branch in the batch approval flow should not depend on the prose returned by check_approvable. Update check_approvable and its caller to propagate or inspect the typed proposals.DeadClaimRefsError, or otherwise use the structured dead-reference result, then pass those proposal IDs through missing_claim_refs and the existing drop_missing_claims prompt while preserving other approval blockers.src/vouch/adopt.py (2)
277-284: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winBare
except Exceptionin retire loop swallows all failures silently.Archiving is intentionally best-effort so a failure shouldn't abort adoption, but catching bare
Exceptionwith no logging means a genuine bug inlifecycle.archive(not just an expected "can't archive this" case) disappears with zero diagnostic trail —report.retiredjust quietly ends up shorter than expected.🩹 Suggested minimal logging
if retire: for claim_id in landed_durable: try: lifecycle.archive(personal, claim_id=claim_id, actor=actor) - except Exception: + except Exception as exc: # Retiring is best-effort tidying of the personal KB; a claim # that cannot be archived must not fail the adoption. + logger.warning("adopt: failed to retire %s: %s", claim_id, exc) continue report.retired.append(claim_id)🤖 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/adopt.py` around lines 277 - 284, Update the retire loop in the adoption flow around lifecycle.archive to log the caught exception with claim_id and sufficient context before continuing. Preserve best-effort behavior so archive failures do not abort adoption, while ensuring unexpected failures leave a diagnostic trail.
166-182: 🎯 Functional Correctness | 🔵 Trivial | ⚖️ Poor tradeoffDry-run preview doesn't model duplicate-content rejection.
dry_runassumesreceipt is not None and gate_openalways lands durable, but the real path (lines 240-258) can still reject as "a duplicate of an already-durable claim" (same content, different id) and fall through toclaims_skippedinstead. Given the explicit design goal in the CLI comment that "a dry run that promises durable claims a closed gate will leave pending is worse than no preview at all," this specific edge case (already-durable-by-content-but-different-id) can still make--dry-runover-promise a durable landing that the real run skips.🤖 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/adopt.py` around lines 166 - 182, Update the dry-run branch in the adoption flow to model duplicate-content rejection consistently with the real path: before appending a claim to claims_durable, detect whether its content already matches an existing durable claim even when the IDs differ, and append it to claims_skipped instead. Reuse the existing duplicate-detection helper or logic from the real processing path near the durable-claim handling, while preserving the current ID and queued-claim checks..github/workflows/vouchbench-season.yml (1)
37-41: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winAdd an explicit least-privilege
permissions:block.This job never comments, pushes, or writes anything back to the repo — only benefits from an explicit
permissions: contents: read(workflow- or job-level) instead of inheriting the repo/org default, which could be broader.🔒 Suggested least-privilege permissions
name: vouchbench-season +permissions: + contents: read + on:🤖 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 @.github/workflows/vouchbench-season.yml around lines 37 - 41, Add an explicit least-privilege permissions block for the score job, granting only contents read access. Place it at workflow or job scope near the jobs/score configuration, without changing the existing execution steps.tests/test_koth_kit.py (1)
8-15: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueFilename doesn't mirror the module under test.
This file exercises
.github/scripts/validate_kit.py; the strict convention would name ittests/test_validate_kit.py.As per coding guidelines, "Tests must mirror module names using the strict
tests/test_<module>.pyconvention."🤖 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 `@tests/test_koth_kit.py` around lines 8 - 15, Rename the test file from test_koth_kit.py to test_validate_kit.py so it follows the strict tests/test_<module>.py convention and mirrors the validate_kit module loaded by the test.Source: Coding guidelines
tests/test_enrich.py (1)
79-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a parsing test for
updates.
Updateentries feedapply_enrich_updates, which supersedes durable claims, but no test here exercises the defensive rules inparse_enrichment: non-stringattribute/old/newskipped, blank values skipped, andold.lower() == new.lower()dropped. Only the happy path is covered indirectly viatests/test_capture_answer.py.As per coding guidelines, "Run and preserve the project test suite, including capability parity tests and feature tests for new methods."
🤖 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 `@tests/test_enrich.py` around lines 79 - 107, Add a focused test for the updates parsing behavior in parse_enrichment, covering valid updates plus skipping entries with non-string attribute/old/new values, blank values, and case-insensitively equal old and new values. Assert only valid, meaningful updates remain and preserve the existing test suite.Source: Coding guidelines
src/vouch/capture.py (2)
570-579: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winLog session answer-capture failures.
capture.pyswallows allcapture_session_answers()exceptions with only_answer_skip(..., "error"), so the summary path always succeeds without making the failure diagnosable. Add a module logger warning around this block withexc_info=True.♻️ Proposed change
- except Exception: - answers = _answer_skip(session_id, "error") + except Exception: + logger.warning( + "capture: session answer extraction failed for %s", session_id, + exc_info=True, + ) + answers = _answer_skip(session_id, "error")🤖 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/capture.py` around lines 570 - 579, Update the exception handler around capture_session_answers in the session-answer path to emit a module logger warning with exc_info=True before returning _answer_skip(session_id, "error"). Preserve the existing fallback behavior and source handling.
825-833: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
proposals._receipts_auto_approve(store)instead of duplicating the self-approval check.
apply_enrich_updatesre-implements the trusted-agent/auto-receipt gate via private_review_config, whilepropagates._receipts_auto_approve()already exposes this predicate inadopt.py. Reusing that helper avoids a parallel write path diverging from the gate decision used by the rest ofcapture.py.🤖 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/capture.py` around lines 825 - 833, The apply_enrich_updates gate currently duplicates the approval predicate through review_cfg and _review_config. Replace that logic with proposals._receipts_auto_approve(store), preserving the existing gate-closed return behavior and leaving the outcomes handling unchanged.Source: Path instructions
src/vouch/strategy.py (1)
235-256: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winSurface why the strategy sandbox failed — failure modes currently all become
None.Timeouts, startup errors, nonzero exits, and malformed output are indistinguishable, and
proc.stderris thrown away. Since-I+env={"PATH": ...}depends onvouchbeing importable from site-packages, an editable checkout without.pthimport paths makespython -m vouch.strategy --childfail silently and treat every strategy as “no reordering”. Logging a truncatedstderrplusreturncodekeeps the non-abort behavior while making the real failure diagnosable.🤖 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/strategy.py` around lines 235 - 256, Update the subprocess handling in the strategy sandbox flow to log diagnosable failure details while still returning None: distinguish timeout, startup OSError, nonzero returncode, and malformed output; include a safely truncated proc.stderr and returncode where available. Preserve the non-abort behavior, and ensure the child-module launch remains usable for editable checkouts by providing the necessary import path in the environment or invocation.src/vouch/compile.py (1)
205-237: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winPhase-A prompt doesn't exclude already-taken topics.
build_topic_prompt(unlikebuild_prompt) never inlinesstore.list_pages()/_pending_page_names(store), so phase A can plan a topic that already exists as a page or pending proposal. Phase B's prompt then simultaneously tells the model to "draft exactly one focused page" for that planned topic (line 303-306) and to not redraft anything in "TAKEN TOPICS" (line 316) — contradictory instructions that, at best, waste one of the two LLM calls two-phase mode is supposed to spend wisely, since_draft_problem's collision guard (line 390-391) will drop the draft anyway.♻️ Suggested fix: give phase A the same taken-topics context as phase B
def build_topic_prompt(store: KBStore, *, max_pages: int) -> str: claims = [ c for c in store.list_claims() if c.status not in _RETRACTED_CLAIM_STATUSES ] if not claims: raise CompileError("nothing to compile: the KB has no live approved claims") + pages = store.list_pages() + pending = _pending_page_names(store) lines = [ "You are planning a project's knowledge wiki. Group the approved", "claims below into a small set of durable, single-subject topics —", "the pages a future reader browses first.", "", "APPROVED CLAIMS (id: text):", ] for c in claims: lines.append(f"- {c.id}: {c.text}") + taken = [p.title for p in pages] + sorted(pending) + if taken: + lines += ["", "ALREADY COVERED — do not propose these as new topics:"] + lines += [f"- {t}" for t in taken] lines += [ "", "RULES",🤖 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/compile.py` around lines 205 - 237, Update build_topic_prompt to include the existing and pending page topics, using store.list_pages() and _pending_page_names(store) consistently with build_prompt. Add this taken-topics context and instruct phase A to exclude those topics from its JSON plan, while preserving the existing max_pages and single-subject topic rules.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 4af4a156-dfd9-4a0b-bd38-169ed0f83867
📒 Files selected for processing (95)
.github/scripts/koth_score.py.github/scripts/score_strategy.py.github/scripts/update_leaderboard.py.github/scripts/validate_kit.py.github/workflows/koth-ledger.yml.github/workflows/vouchbench-season.ymlCHANGELOG.mdCONTRIBUTING.mdREADME.mdadapters/claude-code/README.mdcompetition/LEADERBOARD.mdcompetition/kits/current/kit.yamlcontrib/strategies/README.mdcontrib/strategies/baseline.pycontrib/strategies/example_lexical.pycontrib/strategies/provenance_rank.pydocs/koth-ladder.mddocs/koth-strategy-lane.mddocs/mining-on-vouch.mddocs/vouchbench-seasons.mdschemas/context-item.schema.jsonschemas/context-pack.schema.jsonsrc/vouch/admission.pysrc/vouch/adopt.pysrc/vouch/bench.pysrc/vouch/bundle.pysrc/vouch/capabilities.pysrc/vouch/capture.pysrc/vouch/cli.pysrc/vouch/compile.pysrc/vouch/context.pysrc/vouch/enrich.pysrc/vouch/health.pysrc/vouch/hooks.pysrc/vouch/hot_memory.pysrc/vouch/hub.pysrc/vouch/hub_client.pysrc/vouch/jsonl_server.pysrc/vouch/lifecycle.pysrc/vouch/memory_contract.pysrc/vouch/models.pysrc/vouch/proposals.pysrc/vouch/recall.pysrc/vouch/retrieval_events.pysrc/vouch/server.pysrc/vouch/session_split.pysrc/vouch/storage.pysrc/vouch/strategies/__init__.pysrc/vouch/strategies/provenance.pysrc/vouch/strategy.pysrc/vouch/sync.pysrc/vouch/wiki_render.pysrc/vouch/worthiness.pytests/test_admission.pytests/test_adopt.pytests/test_bench.pytests/test_bundle.pytests/test_capture.pytests/test_capture_answer.pytests/test_capture_scope.pytests/test_cli.pytests/test_codex_rollout.pytests/test_compile.pytests/test_dead_claim_refs.pytests/test_enrich.pytests/test_gated_import.pytests/test_health.pytests/test_hooks.pytests/test_hub.pytests/test_hub_client.pytests/test_install_adapter.pytests/test_koth_kit.pytests/test_memory_contract.pytests/test_prompt_gate_live.pytests/test_retrieval_backend.pytests/test_retrieval_events.pytests/test_session_split.pytests/test_strategy.pytests/test_sync.pytests/test_update_leaderboard.pytests/test_wiki_render.pytests/test_worthiness.pywebapp/plugins/vouch-proxy.test.tswebapp/plugins/vouch-proxy.tswebapp/src/App.tsxwebapp/src/components/Shell.test.tsxwebapp/src/components/Shell.tsxwebapp/src/lib/rpc.test.tswebapp/src/lib/rpc.tswebapp/src/views/PendingView.deadrefs.test.tsxwebapp/src/views/PendingView.multi.test.tsxwebapp/src/views/PendingView.test.tsxwebapp/src/views/PendingView.tsxwebapp/src/views/SessionsView.test.tsxwebapp/src/views/SessionsView.tsx
| # only the gate's own comments are trusted: a scorecard-shaped | ||
| # comment from anyone else must never reach the ledger | ||
| gh api "repos/${GITHUB_REPOSITORY}/issues/${pr}/comments" \ | ||
| --paginate \ | ||
| --jq '.[] | select(.user.login == "github-actions[bot]") | .body' \ | ||
| > /tmp/comments.txt || continue | ||
| rm -f /tmp/report.json | ||
| python3 - <<'PYEOF' | ||
| import re | ||
|
|
||
| body = open("/tmp/comments.txt", encoding="utf-8").read() | ||
| blocks = re.findall(r"```json\n(.*?)\n```", body, flags=re.S) | ||
| reports = [b for b in blocks if '"dethroned": true' in b] | ||
| if reports: | ||
| with open("/tmp/report.json", "w", encoding="utf-8") as fh: | ||
| fh.write(reports[-1]) | ||
| PYEOF |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== repository files =="
git ls-files '.github/workflows/*' | sed -n '1,120p'
echo
echo "== target workflow =="
cat -n .github/workflows/koth-ledger.yml | sed -n '1,130p'
echo
echo "== search for koth-ledger references and workflow actions =="
rg -n "koth-|LEADERBOARD|dethroned|comments|update_leaderboard|GITHUB_TOKEN|pull-requests: write|issues: write|secrets\.KOTH|permissions:" .github/workflows -S || trueRepository: vouchdev/vouch
Length of output: 11676
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== relevant gate workflow snippets =="
sed -n '1,220p' .github/workflows/koth-gate.yml
echo
sed -n '1,150p' .github/workflows/koth-engine-gate.yml
echo
echo "== scripts that parse leaderboard/report =="
fd -a 'update_leaderboard.py' . | while read -r f; do
echo "--- ${f#"$PWD/"}"
cat -n "$f" | sed -n '1,240p'
done
echo
echo "== comments related to dethroned/source of truth =="
rg -n "dethroned|\"dethroned|LEADERBOARD|scorecard|verdict=dethroned|LEADERBOARD.md|report.json|koth-ledger|github-actions\\[bot\\]" .github -S || true
echo
echo "== changed files/stat =="
git diff --stat || true
git diff -- .github/workflows/koth-ledger.yml | sed -n '1,220p' || trueRepository: vouchdev/vouch
Length of output: 21396
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== update_leaderboard.py implementation =="
fd -a 'update_leaderboard.py' .github/scripts | sort | while read -r f; do
echo "--- ${f#"$PWD/"}"
cat -n "$f" | sed -n '1,260p'
done
echo
echo "== other workflows with issue/pull-request write and comment creation =="
python3 - <<'PY'
from pathlib import Path
import re
patterns = {
"pull_request_target_or_pr_event": re.compile(r"^\s+pull_request(_target)?:\s*$|^\s+pull_request:", re.M),
"issue_or_pr_write": re.compile(r"^\s+(issues|pull-requests|issues\s*,\s*pull-requests):\s*(write|read,?\s*write)", re.M),
"gh_(cli_)?comment": re.compile(r"\b(g\s*gh\s+|gh\s+)(?:[Aa].*\bcomment\b|pr\s+comment\b|issue\s+comment\b)", re.S),
}
for p in sorted(Path(".github/workflows").glob("*.yml")):
text = p.read_text()
print(f"\n--- {p} ---")
for name,pattern in patterns.items():
m = pattern.search(text)
if m:
start = min(text.count("\n", 0, m.start())+1, len(text.splitlines()))
print(f"{name}: line {start}; text:\n{m.group(0)!r}")
PYRepository: vouchdev/vouch
Length of output: 17327
Anchor the ledger scorecard to the known scoring run, not just the bot login.
user.login == "github-actions[bot]" also covers comments from any same-repo GITHUB_TOKEN workflow with pull-requests: write; another koth-* gate scorecard containing the same ```json + "dethroned": true shape can reach the ledger unchanged. Anchor trust to workflow-specific metadata, e.g. comment authorship tied to a successful koth-gate.yml run/SHA plus a separate secret/HMAC signing the report.
🤖 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 @.github/workflows/koth-ledger.yml around lines 56 - 72, Update the
comment-filtering and report-selection logic around the GitHub API query and
Python extraction so the ledger accepts scorecards only from the specific
successful koth-gate.yml run and commit SHA, not merely from
github-actions[bot]. Validate the report with workflow-specific metadata and the
configured secret/HMAC signature before writing /tmp/report.json, rejecting
same-shaped reports from other workflows.
| def generate(seed: int, *, sessions: int = DEFAULT_SESSIONS) -> Dataset: | ||
| """Build the deterministic dataset for ``seed``. | ||
|
|
||
| Each of the seven categories gets one attribute from the pool (seeded | ||
| shuffle), its statements planted across ``sessions`` session documents, | ||
| and filler prose everywhere so retrieval has a real haystack. | ||
| """ | ||
| rng = random.Random(seed) | ||
| attrs = list(_ATTRIBUTES) | ||
| rng.shuffle(attrs) | ||
| docs = _Sessions(sessions, rng) | ||
| cases: list[MemoryCase] = [] | ||
|
|
||
| def spot(exclude: int | None = None) -> int: | ||
| idx = rng.randrange(sessions) | ||
| while idx == exclude: | ||
| idx = rng.randrange(sessions) | ||
| return idx |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
sessions < 2 hangs or crashes; validate the floor.
spot(exclude=home) spins forever when sessions == 1 (every draw equals exclude), and rng.randrange(sessions - 1) at Lines 298 and 363 raises ValueError on randrange(0). sessions is a public kwarg on generate/run/run_seeds, so a caller passing 1 gets an infinite loop rather than a clear error.
🐛 Proposed guard
rng = random.Random(seed)
+ if sessions < 2:
+ raise ValueError(f"sessions must be >= 2 (the update cases need two), got {sessions}")
attrs = list(_ATTRIBUTES)📝 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.
| def generate(seed: int, *, sessions: int = DEFAULT_SESSIONS) -> Dataset: | |
| """Build the deterministic dataset for ``seed``. | |
| Each of the seven categories gets one attribute from the pool (seeded | |
| shuffle), its statements planted across ``sessions`` session documents, | |
| and filler prose everywhere so retrieval has a real haystack. | |
| """ | |
| rng = random.Random(seed) | |
| attrs = list(_ATTRIBUTES) | |
| rng.shuffle(attrs) | |
| docs = _Sessions(sessions, rng) | |
| cases: list[MemoryCase] = [] | |
| def spot(exclude: int | None = None) -> int: | |
| idx = rng.randrange(sessions) | |
| while idx == exclude: | |
| idx = rng.randrange(sessions) | |
| return idx | |
| def generate(seed: int, *, sessions: int = DEFAULT_SESSIONS) -> Dataset: | |
| """Build the deterministic dataset for ``seed``. | |
| Each of the seven categories gets one attribute from the pool (seeded | |
| shuffle), its statements planted across ``sessions`` session documents, | |
| and filler prose everywhere so retrieval has a real haystack. | |
| """ | |
| rng = random.Random(seed) | |
| if sessions < 2: | |
| raise ValueError(f"sessions must be >= 2 (the update cases need two), got {sessions}") | |
| attrs = list(_ATTRIBUTES) | |
| rng.shuffle(attrs) | |
| docs = _Sessions(sessions, rng) | |
| cases: list[MemoryCase] = [] | |
| def spot(exclude: int | None = None) -> int: | |
| idx = rng.randrange(sessions) | |
| while idx == exclude: | |
| idx = rng.randrange(sessions) | |
| return idx |
🧰 Tools
🪛 ast-grep (0.44.1)
[info] 256-256: use secrets package over random package
Context: random.Random(seed)
Note: [CWE-330] Use of Insufficiently Random Values.
(avoid-random-python)
🤖 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/bench.py` around lines 250 - 267, Validate that the public sessions
parameter is at least 2 at the start of generate, rejecting smaller values with
a clear ValueError before constructing _Sessions or invoking spot. Ensure the
validation applies through the existing generate/run/run_seeds call path and
prevents both the spot infinite loop and randrange(0) failures.
| for m in members: | ||
| if m.name.startswith("evidence/") and m.name.endswith(".yaml"): | ||
| ev = Evidence.model_validate( | ||
| yaml.safe_load(tar.extractfile(m).read()) # type: ignore[union-attr] | ||
| ) | ||
| store.put_evidence(ev) | ||
| evidence_registered += 1 |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Re-importing an overlapping bundle aborts the whole import.
store.put_evidence raises ValueError when the evidence id already exists (src/vouch/storage.py:990-1001). hub_client.pull calls import_as_proposals on every pull, so the second pull of a bundle that re-ships already-registered evidence dies here — after put_source has already run and before the audit event is written, leaving a partial, unlogged import. put_source is idempotent; put_evidence is not.
🛡️ Proposed fix
if m.name.startswith("evidence/") and m.name.endswith(".yaml"):
ev = Evidence.model_validate(
yaml.safe_load(tar.extractfile(m).read()) # type: ignore[union-attr]
)
- store.put_evidence(ev)
- evidence_registered += 1
+ try:
+ store.get_evidence(ev.id)
+ except ArtifactNotFoundError:
+ store.put_evidence(ev)
+ evidence_registered += 1ArtifactNotFoundError comes from .storage; add it to the local import at Line 938.
📝 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.
| for m in members: | |
| if m.name.startswith("evidence/") and m.name.endswith(".yaml"): | |
| ev = Evidence.model_validate( | |
| yaml.safe_load(tar.extractfile(m).read()) # type: ignore[union-attr] | |
| ) | |
| store.put_evidence(ev) | |
| evidence_registered += 1 | |
| for m in members: | |
| if m.name.startswith("evidence/") and m.name.endswith(".yaml"): | |
| ev = Evidence.model_validate( | |
| yaml.safe_load(tar.extractfile(m).read()) # type: ignore[union-attr] | |
| ) | |
| try: | |
| store.get_evidence(ev.id) | |
| except ArtifactNotFoundError: | |
| store.put_evidence(ev) | |
| evidence_registered += 1 |
🤖 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/bundle.py` around lines 966 - 972, Update the evidence import loop
in import_as_proposals to catch the existing-evidence condition from
store.put_evidence and continue processing overlapping bundle members instead of
aborting the import; import and use ArtifactNotFoundError from .storage as the
requested local exception handling dependency, while preserving
evidence_registered updates only for newly stored evidence.
| def save_token(url: str, token: str) -> None: | ||
| path = _creds_path() | ||
| path.parent.mkdir(parents=True, exist_ok=True) | ||
| data: dict[str, Any] = {} | ||
| if path.exists(): | ||
| data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} | ||
| data.setdefault("tokens", {})[url.rstrip("/")] = token | ||
| path.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8") | ||
| path.chmod(0o600) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Token is written world-readable before the chmod lands.
write_text creates the file under the process umask (typically 0644), so the bearer token exists on disk readable by other local users until chmod(0o600) runs. Create it restricted instead.
🔒 Proposed fix
data.setdefault("tokens", {})[url.rstrip("/")] = token
- path.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8")
- path.chmod(0o600)
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
+ f.write(yaml.safe_dump(data, sort_keys=False))
+ # existing files keep their old mode through O_CREAT; tighten explicitly.
+ path.chmod(0o600)📝 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.
| def save_token(url: str, token: str) -> None: | |
| path = _creds_path() | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| data: dict[str, Any] = {} | |
| if path.exists(): | |
| data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} | |
| data.setdefault("tokens", {})[url.rstrip("/")] = token | |
| path.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8") | |
| path.chmod(0o600) | |
| def save_token(url: str, token: str) -> None: | |
| path = _creds_path() | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| data: dict[str, Any] = {} | |
| if path.exists(): | |
| data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} | |
| data.setdefault("tokens", {})[url.rstrip("/")] = token | |
| fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) | |
| with os.fdopen(fd, "w", encoding="utf-8") as f: | |
| f.write(yaml.safe_dump(data, sort_keys=False)) | |
| # existing files keep their old mode through o_creat; tighten explicitly. | |
| path.chmod(0o600) |
🤖 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/hub_client.py` around lines 107 - 115, Update save_token to create
or open the credentials file with restrictive 0o600 permissions before writing
its contents, rather than relying on write_text followed by chmod. Preserve the
existing YAML data handling and token storage behavior, and retain chmod as a
final permission safeguard if appropriate.
| if isinstance(personal, dict) and "fallback_capture" in personal: | ||
| new_text, n = re.subn( | ||
| r"(?m)^(\s*fallback_capture:\s*).*$", | ||
| rf"\g<1>{value}", | ||
| text, | ||
| count=1, | ||
| ) | ||
| if n == 1: | ||
| cfg_path.write_text(new_text, encoding="utf-8") | ||
| return | ||
| elif isinstance(personal, dict): | ||
| new_text, n = re.subn( | ||
| r"(?m)^personal:[ \t]*$", | ||
| f"personal:\n fallback_capture: {value}", | ||
| text, | ||
| count=1, | ||
| ) | ||
| if n == 1: | ||
| cfg_path.write_text(new_text, encoding="utf-8") | ||
| return |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Validate the regex-edited text before writing — a mismatched-indent config can be corrupted.
The elif isinstance(personal, dict) branch inserts fallback_capture at a hardcoded 2-space indent under personal:. If the existing block's children are indented differently (e.g. 4 spaces), the rewritten file is no longer parseable YAML, and the write happens unconditionally. Similarly, the ^(\s*fallback_capture:\s*) pattern is matched against the whole file, so a same-named key in another block would be rewritten instead. Cheapest robust guard: re-parse the candidate text and confirm it yields the expected value, else fall through to the structural rewrite that already exists below.
🛡️ Proposed fix
+ def _verified(candidate: str) -> bool:
+ try:
+ parsed = yaml.safe_load(candidate)
+ except yaml.YAMLError:
+ return False
+ block = parsed.get("personal") if isinstance(parsed, dict) else None
+ return isinstance(block, dict) and block.get("fallback_capture") is enabled
+
if isinstance(personal, dict) and "fallback_capture" in personal:
new_text, n = re.subn(
r"(?m)^(\s*fallback_capture:\s*).*$",
rf"\g<1>{value}",
text,
count=1,
)
- if n == 1:
+ if n == 1 and _verified(new_text):
cfg_path.write_text(new_text, encoding="utf-8")
return
elif isinstance(personal, dict):
new_text, n = re.subn(
r"(?m)^personal:[ \t]*$",
f"personal:\n fallback_capture: {value}",
text,
count=1,
)
- if n == 1:
+ if n == 1 and _verified(new_text):
cfg_path.write_text(new_text, encoding="utf-8")
return🤖 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/hub.py` around lines 424 - 443, In the fallback_capture update
logic, validate the regex-produced candidate YAML before cfg_path.write_text:
re-parse it and confirm the personal.fallback_capture value matches the
requested value, otherwise do not write it. Scope the replacement to the
fallback_capture key within the personal block rather than any same-named key
elsewhere, and derive insertion indentation from the existing personal block
instead of hardcoding two spaces; preserve the existing structural rewrite
fallback for unsuccessful validation.
| page.claims = [c for c in page.claims if c not in dead] | ||
| page.body = strip_claim_markers(page.body, dead) | ||
| page.updated_at = datetime.now(UTC) | ||
| store.update_page(page) | ||
| with index_db.open_db(store.kb_dir) as conn: | ||
| index_db.index_page( | ||
| conn, id=page.id, title=page.title, body=page.body, | ||
| type=page.type, tags=page.tags, | ||
| ) | ||
| for prop in store.list_proposals(ProposalStatus.PENDING): | ||
| if prop.kind != ProposalKind.PAGE: | ||
| continue | ||
| refs = prop.payload.get("claims") or [] | ||
| dead = [c for c in refs if not store._claim_path(c).exists()] | ||
| if not dead: | ||
| continue | ||
| result.proposals[prop.id] = dead | ||
| if dry_run: | ||
| continue | ||
| prop.payload["claims"] = [c for c in refs if c not in dead] | ||
| body = prop.payload.get("body") | ||
| if isinstance(body, str): | ||
| prop.payload["body"] = strip_claim_markers(body, dead) | ||
| store.update_proposal(prop) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
route this cleanup through the review gate.
This new MCP/JSONL/CLI write path rewrites durable pages and pending proposals directly, without proposals.approve(). An audit event does not replace reviewer approval; generate reviewed cleanup proposals or otherwise route the changes through the established approval path.
As per coding guidelines, “All writes must pass through the reviewed proposal approval path, specifically proposals.approve(); do not add parallel data paths that bypass the review gate.”
🤖 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/lifecycle.py` around lines 269 - 292, The cleanup logic in the page
and pending-proposal handling directly persists changes through
store.update_page and store.update_proposal, bypassing the reviewed approval
gate. Refactor this path to generate reviewed cleanup proposals and apply
changes exclusively through proposals.approve(), preserving the existing dry-run
and dead-claim cleanup behavior without introducing a parallel write path.
Sources: Coding guidelines, Path instructions
| page.claims = [c for c in page.claims if c not in dead] | ||
| page.body = strip_claim_markers(page.body, dead) | ||
| page.updated_at = datetime.now(UTC) | ||
| store.update_page(page) | ||
| with index_db.open_db(store.kb_dir) as conn: | ||
| index_db.index_page( | ||
| conn, id=page.id, title=page.title, body=page.body, | ||
| type=page.type, tags=page.tags, | ||
| ) | ||
| for prop in store.list_proposals(ProposalStatus.PENDING): | ||
| if prop.kind != ProposalKind.PAGE: | ||
| continue | ||
| refs = prop.payload.get("claims") or [] | ||
| dead = [c for c in refs if not store._claim_path(c).exists()] | ||
| if not dead: | ||
| continue | ||
| result.proposals[prop.id] = dead | ||
| if dry_run: | ||
| continue | ||
| prop.payload["claims"] = [c for c in refs if c not in dead] | ||
| body = prop.payload.get("body") | ||
| if isinstance(body, str): | ||
| prop.payload["body"] = strip_claim_markers(body, dead) | ||
| store.update_proposal(prop) | ||
| if not dry_run and (result.pages or result.proposals): | ||
| audit.log_event( | ||
| store.kb_dir, | ||
| event="page.dead_refs_wipe", | ||
| actor=actor, | ||
| object_ids=[*result.pages, *result.proposals], | ||
| data={ | ||
| "pages": result.pages, | ||
| "proposals": result.proposals, | ||
| "dropped": result.dropped, | ||
| }, | ||
| ) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
avoid unaudited partial cleanup.
Pages and proposals are rewritten incrementally, but the authoritative audit event is emitted only after every write succeeds. A later write or reindex failure leaves earlier durable changes applied with no audit record. Stage the full change set and use a recoverable/transactional commit strategy that records every applied mutation.
As per path instructions, “Audit log (audit.log.jsonl) is authoritative; never edit decided/ or audit JSONL by hand.”
🤖 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/lifecycle.py` around lines 269 - 304, Update the cleanup flow
containing the page rewrites, proposal updates, and final page.dead_refs_wipe
audit event to avoid applying mutations incrementally before auditing. Stage the
complete page and proposal change set, then commit it through a recoverable or
transactional strategy that records every mutation in the authoritative audit
log, including partial-failure recovery; do not modify decided/ or audit JSONL
directly.
Source: Path instructions
| path = kb_dir / INSTANCE_ID_FILENAME | ||
| try: | ||
| existing = path.read_text(encoding="utf-8").strip() | ||
| except OSError: | ||
| existing = "" | ||
| if existing: | ||
| return existing | ||
| new_id = uuid.uuid4().hex | ||
| path.write_text(new_id + "\n", encoding="utf-8") | ||
| return new_id |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Unlocked read-mint-write: concurrent callers mint different instance ids.
ensure_identity (Lines 487-503) deliberately serializes exactly this sequence under _audit_lock because two racing hooks otherwise "mint different ids, and every artifact stamped with the loser's id becomes unreachable under the winner's identity." This function has the same read-then-write shape with no lock, and it is reached from bundle.export (Line 195) and sync._source_origin, both of which hooks can run concurrently — so bundles/proposals can be stamped with an id that is not the one left on disk.
Separately: the sidecar is not in the .gitignore written at Lines 441-445, so a clone inherits the source KB's instance id, which conflicts with the docstring's "two KBs holding identical artifacts still have different instance ids." Worth confirming which behavior is intended.
🔒 Proposed fix for the race
path = kb_dir / INSTANCE_ID_FILENAME
try:
existing = path.read_text(encoding="utf-8").strip()
except OSError:
existing = ""
if existing:
return existing
- new_id = uuid.uuid4().hex
- path.write_text(new_id + "\n", encoding="utf-8")
- return new_id
+ from .audit import _audit_lock
+
+ with _audit_lock(kb_dir):
+ try:
+ existing = path.read_text(encoding="utf-8").strip()
+ except OSError:
+ existing = ""
+ if existing:
+ return existing # lost the race — adopt the winner's id
+ new_id = uuid.uuid4().hex
+ path.write_text(new_id + "\n", encoding="utf-8")
+ return new_id📝 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.
| path = kb_dir / INSTANCE_ID_FILENAME | |
| try: | |
| existing = path.read_text(encoding="utf-8").strip() | |
| except OSError: | |
| existing = "" | |
| if existing: | |
| return existing | |
| new_id = uuid.uuid4().hex | |
| path.write_text(new_id + "\n", encoding="utf-8") | |
| return new_id | |
| path = kb_dir / INSTANCE_ID_FILENAME | |
| try: | |
| existing = path.read_text(encoding="utf-8").strip() | |
| except OSError: | |
| existing = "" | |
| if existing: | |
| return existing | |
| from .audit import _audit_lock | |
| with _audit_lock(kb_dir): | |
| try: | |
| existing = path.read_text(encoding="utf-8").strip() | |
| except OSError: | |
| existing = "" | |
| if existing: | |
| return existing # lost the race — adopt the winner's id | |
| new_id = uuid.uuid4().hex | |
| path.write_text(new_id + "\n", encoding="utf-8") | |
| return new_id |
🤖 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/storage.py` around lines 300 - 309, Serialize the read-mint-write
sequence in the identity helper used by bundle.export and sync._source_origin,
reusing the existing _audit_lock pattern from ensure_identity so concurrent
callers return the same on-disk instance ID. Also verify the intended clone
behavior for the instance-ID sidecar and update the .gitignore handling only if
the project contract requires cloned KBs to receive distinct identities.
| def rank(query: str, candidates: list[Candidate], *, limit: int) -> list[str]: | ||
| q = _tokens(query) | ||
| first_person = bool(_FIRST_PERSON_QUERY.search(query)) | ||
|
|
||
| def key(c: Candidate) -> float: | ||
| text = c.summary | ||
| overlap = len(q & _tokens(text)) / len(q) if q else 0.0 | ||
| score = 0.7 * c.score + 0.3 * overlap | ||
| if _INSTRUCTION.search(text): | ||
| score -= 10.0 | ||
| if _HEARSAY.search(text) and first_person: | ||
| score -= 5.0 | ||
| elif _HEARSAY.search(text): | ||
| score -= 2.0 | ||
| if _UPDATE.search(text): | ||
| score += 1.0 | ||
| return score | ||
|
|
||
| return [c.id for c in sorted(candidates, key=key, reverse=True)] |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n -A5 'def rank' contrib/strategies/*.py src/vouch/strategy.py src/vouch/strategies/*.py
ast-grep outline src/vouch/strategy.py --items allRepository: vouchdev/vouch
Length of output: 3712
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== strategies provenance files =="
cat -n src/vouch/strategies/provenance.py | sed -n '1,140p'
printf '\n== contrib provenance rank ==\n'
cat -n contrib/strategies/provenance_rank.py | sed -n '1,120p'
printf '\n== example lexical for comparison ==\n'
cat -n contrib/strategies/example_lexical.py | sed -n '1,90p'
printf '\n== all rank callsites =="
rg -n '\brank\(' src tests scripts contrib -g '*.py' || true
printf '\n== retrieval strategy usages around limit =="
rg -n -A7 -B7 'rank\(|ordered_ids|apply_ordering|RetrievalStrategy|load_dotted|load_from_path' src/vouch/strategy.py src tests -g '*.py' || trueRepository: vouchdev/vouch
Length of output: 8981
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== rank callsites =="
rg -n '\brank\(' . -g '*.py' || true
echo
echo "== retrieval strategy usages =="
rg -n -A7 -B7 'rank\(|ordered_ids|apply_ordering|RetrievalStrategy|load_dotted|load_from_path' . -g '*.py' || true
echo
echo "== deterministic signature and limit behavior probe from source text =="
python3 - <<'PY'
from pathlib import Path
paths = ["src/vouch/strategies/provenance.py", "contrib/strategies/provenance_rank.py", "contrib/strategies/example_lexical.py"]
for path in paths:
text = Path(path).read_text()
start = text.index("def rank(query: str, candidates: list[Candidate], *, limit: int) -> list[str]:")
end = text.index("\n return ", start) + len("return ")
body = text[start:end]
print(f"\n{path}:")
print("uses_limit_parameter=" + ("limit" in body))
print("uses_limit_value=" + ("[:limit]" in body or "(" in text[start:text.find("return ", start)] and "limit" in text[start:text.find("return ", start)]) )
print("returns_unbound_sorted=" + (":[None" not in body and "sorted(" in body and "[:limit]" not in body))
PYRepository: vouchdev/vouch
Length of output: 48015
limit is accepted but never applied in rank().
src/vouch/context.py:_maybe_strategy() is built around the top budget: it over-fetches candidates into a window, calls strat.rank(..., limit=limit), then drops de-prioritized ids below that window. This strategy returns every candidate id instead, so low-scoring hits can remain in the output when the context budget is small.
🐛 Proposed fix
- return [c.id for c in sorted(candidates, key=key, reverse=True)]
+ return [c.id for c in sorted(candidates, key=key, reverse=True)][:limit]📝 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.
| def rank(query: str, candidates: list[Candidate], *, limit: int) -> list[str]: | |
| q = _tokens(query) | |
| first_person = bool(_FIRST_PERSON_QUERY.search(query)) | |
| def key(c: Candidate) -> float: | |
| text = c.summary | |
| overlap = len(q & _tokens(text)) / len(q) if q else 0.0 | |
| score = 0.7 * c.score + 0.3 * overlap | |
| if _INSTRUCTION.search(text): | |
| score -= 10.0 | |
| if _HEARSAY.search(text) and first_person: | |
| score -= 5.0 | |
| elif _HEARSAY.search(text): | |
| score -= 2.0 | |
| if _UPDATE.search(text): | |
| score += 1.0 | |
| return score | |
| return [c.id for c in sorted(candidates, key=key, reverse=True)] | |
| def rank(query: str, candidates: list[Candidate], *, limit: int) -> list[str]: | |
| q = _tokens(query) | |
| first_person = bool(_FIRST_PERSON_QUERY.search(query)) | |
| def key(c: Candidate) -> float: | |
| text = c.summary | |
| overlap = len(q & _tokens(text)) / len(q) if q else 0.0 | |
| score = 0.7 * c.score + 0.3 * overlap | |
| if _INSTRUCTION.search(text): | |
| score -= 10.0 | |
| if _HEARSAY.search(text) and first_person: | |
| score -= 5.0 | |
| elif _HEARSAY.search(text): | |
| score -= 2.0 | |
| if _UPDATE.search(text): | |
| score += 1.0 | |
| return score | |
| return [c.id for c in sorted(candidates, key=key, reverse=True)][:limit] |
🤖 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/strategies/provenance.py` around lines 65 - 83, Apply the limit
parameter in rank() after sorting candidates by the existing key, returning only
the top limit candidate IDs while preserving the current ranking order and
scoring behavior.
| def _source_origin(src: _SyncSource) -> str: | ||
| """Provenance label for a sync source: the bundle's manifest kb_id or the | ||
| source KB's instance id, falling back to its content-hash source_id.""" | ||
| if src.bundle_path is not None: | ||
| return bundle._manifest_kb_id(src.bundle_path) or src.source_id | ||
| if src.root is not None: | ||
| from .storage import read_or_create_instance_id | ||
|
|
||
| return read_or_create_instance_id(src.root) | ||
| return src.source_id |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# what does _SyncSource.root hold — the project root or the .vouch dir?
ast-grep run --pattern 'class _SyncSource:
$$$' --lang python src/vouch/sync.py
rg -nP -C4 '\broot\s*=' src/vouch/sync.py | rg -n -C4 '_load_source|_SyncSource'
ast-grep run --pattern 'def _load_source($$$):
$$$' --lang python src/vouch/sync.py
# and how every other caller passes kb_dir into it
rg -nP -C2 '\bread_or_create_instance_id\s*\(' srcRepository: vouchdev/vouch
Length of output: 1450
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== sync.py outline =="
ast-grep outline src/vouch/sync.py --match _SyncSource --view expanded
ast-grep outline src/vouch/sync.py --match _load_source --view expanded
ast-grep outline src/vouch/sync.py --match _source_origin --view expanded
echo "== relevant sync.py lines 45-160 and 280-305 =="
sed -n '45,160p' src/vouch/sync.py | nl -ba -v45
printf '\n---\n'
sed -n '280,305p' src/vouch/sync.py | nl -ba -v280
echo "== storage.py relevant lines 260-315 =="
sed -n '260,315p' src/vouch/storage.py | nl -ba -v260
echo "== bundle.py relevant export lines 180-205 =="
sed -n '180,205p' src/vouch/bundle.py | nl -ba -v180
echo "== all kb_dir usages around source loading =="
rg -n -C5 'kb_dir|source_path|load_source|_SyncSource' src/vouch/sync.pyRepository: vouchdev/vouch
Length of output: 1009
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== sync.py relevant loading and origin paths =="
python3 - <<'PY'
from pathlib import Path
p = Path('src/vouch/sync.py')
lines = p.read_text().splitlines()
for i in range(45, 165):
print(f"{i:4d}: {lines[i-1]}")
print("\n---")
for i in range(280, 306):
print(f"{i:4d}: {lines[i-1]}")
print("\n---")
for i in range(130, 190):
print(f"{i:4d}: {lines[i-1]}")
PY
echo "== storage.py relevant path implementation =="
python3 - <<'PY'
from pathlib import Path
p = Path('src/vouch/storage.py')
lines = p.read_text().splitlines()
for i in range(250, 320):
print(f"{i:4d}: {lines[i-1]}")
PY
echo "== bundle.py export reference =="
python3 - <<'PY'
from pathlib import Path
p = Path('src/vouch/bundle.py')
lines = p.read_text().splitlines()
for i in range(170, 210):
print(f"{i:4d}: {lines[i-1]}")
PYRepository: vouchdev/vouch
Length of output: 13231
Use a read-only source-provenance lookup.
src.root is already the .vouch dir, but read_or_create_instance_id() creates and writes instance_id when absent. Sync reads should not mutate the source KB; add/read a sidecar without writing, then fall back to src.source_id.
🤖 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/sync.py` around lines 291 - 300, Update _source_origin for sources
with src.root to use a read-only instance-ID lookup that does not create or
write the instance_id sidecar; return the existing ID when present, otherwise
fall back to src.source_id. Do not mutate the source KB during sync, and remove
the read_or_create_instance_id usage from this path.
What changed
Why
What might break
VEP
Tests
make checkpasses locally (lint + mypy + pytest)CHANGELOG.mdupdated under## [Unreleased]Summary by CodeRabbit