mirror of
https://github.com/expo/code-review-cli.git
synced 2026-11-14 09:58:11 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da80f6d3ca | ||
|
|
62c013a321 | ||
|
|
65ffc81014 |
No files matched your search
@@ -31,6 +31,15 @@
|
||||
"timeoutMs": 30000
|
||||
},
|
||||
|
||||
// Active Jev verification cascade. Confident local judgments act immediately;
|
||||
// ambiguous and protected findings defer to the reasoning verifier.
|
||||
"jev": {
|
||||
"model": "jev-1.13.0",
|
||||
"minConfidence": 0.9,
|
||||
"timeoutMs": 10000,
|
||||
"maxContextChars": 30000
|
||||
},
|
||||
|
||||
// Large diffs are split into focused chunks by changed-line count, plus a
|
||||
// cross-cutting pass for multi-file issues. Diffs under maxChangedLines are one
|
||||
// full-context pass. Defaults shown; raise/lower per your model + PR sizes.
|
||||
|
||||
@@ -90,5 +90,7 @@ jobs:
|
||||
CLAUDE_CODE_REVIEW_SHARED_API_TOKEN: ${{ secrets.CLAUDE_CODE_REVIEW_SHARED_API_TOKEN }}
|
||||
# Search-only credential used for fixed, site-scoped documentation discovery.
|
||||
BRAVE_SEARCH_API_KEY: ${{ secrets.BRAVE_SEARCH_API_KEY }}
|
||||
# Shadow-only Jev evaluator; unused while config.jev.enabled is false.
|
||||
TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }}
|
||||
# Optional: override the model for every agent.
|
||||
REVIEWER_MODEL: ${{ vars.REVIEWER_MODEL }}
|
||||
@@ -283,6 +283,35 @@ The boundary, in brief:
|
||||
Full detail — providers, query grammar, `fetch_platform_doc` modes, provenance and
|
||||
citation grounding: [LLP 0013](./llp/0013-platform-research.explainer.md).
|
||||
|
||||
## Jev selective verification
|
||||
|
||||
ECR can use [TypeSafe AI's Jev](https://docs.typesafe.ai/) as the fast first stage of
|
||||
finding verification. Generative agents still discover and explain problems. Jev
|
||||
answers one narrow typed question against bounded local source, then either keeps a
|
||||
clearly supported finding, drops a clearly contradicted ordinary finding, or defers
|
||||
to the existing reasoning verifier.
|
||||
|
||||
There is no shadow mode or separate `enabled` flag. Add the root-only block and provide
|
||||
`TYPESAFE_API_KEY`; removing the block restores the original verification path:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"jev": {
|
||||
"model": "jev-1.13.0",
|
||||
"minConfidence": 0.9,
|
||||
"timeoutMs": 10000,
|
||||
"maxContextChars": 30000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each request contains one finding and bounded source from its file. Critical, security,
|
||||
secrets, and cited findings are never dropped by Jev alone. Low confidence, missing
|
||||
context, credentials, timeouts, and provider errors fall back to the reasoning or
|
||||
original verification path. Confidence is distribution concentration, not proof, so
|
||||
the model and threshold are explicit. See [LLP 0014](./llp/0014-jev-selective-verification.explainer.md)
|
||||
for the research, decision table, and threat model.
|
||||
|
||||
## Monorepos (routing manifest)
|
||||
|
||||
A monorepo can route different subtrees to different reviewer rosters from a single
|
||||
@@ -355,9 +384,9 @@ your-monorepo/
|
||||
|
||||
Enforced in code and by an independent CI guard step, not by convention:
|
||||
|
||||
- **auth and research are locked to the root.** `tokenEnv` is honored only in the
|
||||
- **auth, research, and Jev are locked to the root.** `tokenEnv` is honored only in the
|
||||
root `config.jsonc` / `routing.jsonc` `defaults.auth`; a scope config declaring
|
||||
`auth`/`breakGlass`/`research` fails to parse, and the CI guard sweeps every
|
||||
`auth`/`breakGlass`/`research`/`jev` fails to parse, and the CI guard sweeps every
|
||||
config file repo-wide and refuses to run unless `tokenEnv` appears exactly once,
|
||||
root-owned, equal to `ECR_EXPECTED_TOKEN_ENV`. Routing globs choose *which
|
||||
roster* reviews a file, never *which secret* is sent.
|
||||
@@ -763,20 +792,25 @@ fallback key; `ecr doctor` diagnoses setup.
|
||||
} }
|
||||
```
|
||||
|
||||
- **Meta / Muse Spark 1.2 (public Model API).** Set the default model and any
|
||||
model-pinned agent frontmatter to `meta/muse-spark-1.2`. ECR supplies the fixed
|
||||
- **Meta / Muse Spark 1.3 (public Model API).** Set the default model and any
|
||||
model-pinned agent frontmatter to `meta/muse-spark-1.3`. ECR supplies the fixed
|
||||
public Responses endpoint and the `@ai-sdk/openai` adapter; the generic
|
||||
Chat-Completions compatibility adapter is intentionally not used because it
|
||||
loses Muse's reasoning continuity across tool turns. Standard and Contributor
|
||||
model ids are supported.
|
||||
|
||||
```jsonc
|
||||
"model": "meta/muse-spark-1.2",
|
||||
"model": "meta/muse-spark-1.3",
|
||||
"auth": { "providers": {
|
||||
"meta": { "mode": "api-key", "tokenEnv": "META_API_KEY" }
|
||||
} }
|
||||
```
|
||||
|
||||
Standard and Contributor 1.2/1.3 model ids have known capability metadata. Any
|
||||
future model explicitly named as `meta/<model>` is still routed to the fixed Meta
|
||||
endpoint with neutral metadata, so a renamed family such as `meta/muse-foo` does
|
||||
not require an ECR release; Meta validates an unknown id on its first request.
|
||||
|
||||
Create the key in the [Meta AI developer portal](https://developer.meta.com/ai/).
|
||||
Locally, export it as `META_API_KEY`. In each review-running workflow, replace
|
||||
the Anthropic credential line with
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "1.30.0",
|
||||
"@opencode-ai/sdk": "1.18.4",
|
||||
"@typesafe-ai/sdk": "0.6.0",
|
||||
"cheerio": "1.1.2",
|
||||
"minisearch": "7.2.0",
|
||||
"opencode-ai": "1.18.4",
|
||||
@@ -109,6 +110,8 @@
|
||||
|
||||
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA=="],
|
||||
|
||||
"@typesafe-ai/sdk": ["@typesafe-ai/[email protected]", "", {}, "sha512-IddX+Q0XM+VagOUZFeP7wZjaO4SHMdvnh2zEBdrZZnXedWI3BNK1lKhMx3ayrkFWvVLbVcUHJy6AVZlY+e6Jaw=="],
|
||||
|
||||
"accepts": ["[email protected]", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"ajv": ["[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
**Author:** Philippe Loulidi / Claude
|
||||
**Date:** 2026-07-30
|
||||
**Role:** Root
|
||||
**Related:** [LLP 0001](0001-trust-model.principles.md), [LLP 0002](0002-review-engine-pipeline.explainer.md), [LLP 0003](0003-model-runtimes-and-credentials.explainer.md), [LLP 0004](0004-diff-noise-and-prompts.explainer.md), [LLP 0005](0005-verification-fingerprints-rendering.explainer.md), [LLP 0006](0006-config-schema-loading-routing.explainer.md), [LLP 0007](0007-cli-commands-and-ci.explainer.md), [LLP 0008](0008-sources-and-reporters.explainer.md), [LLP 0009](0009-adoption-templates-and-ci-workflows.guide.md), [LLP 0013](0013-platform-research.explainer.md)
|
||||
**Related:** [LLP 0001](0001-trust-model.principles.md), [LLP 0002](0002-review-engine-pipeline.explainer.md), [LLP 0003](0003-model-runtimes-and-credentials.explainer.md), [LLP 0004](0004-diff-noise-and-prompts.explainer.md), [LLP 0005](0005-verification-fingerprints-rendering.explainer.md), [LLP 0006](0006-config-schema-loading-routing.explainer.md), [LLP 0007](0007-cli-commands-and-ci.explainer.md), [LLP 0008](0008-sources-and-reporters.explainer.md), [LLP 0009](0009-adoption-templates-and-ci-workflows.guide.md), [LLP 0013](0013-platform-research.explainer.md), [LLP 0014](0014-jev-selective-verification.explainer.md)
|
||||
|
||||
This is the root map for `@expo/code-review-cli` (`ecr`). It records what the system does, how the subsystems divide, the invariants that hold across all of them, and where each concern's rationale lives. It is intentionally thin: every subsystem's detail belongs in the doc that owns it. Read this first, then follow the link for the part you care about.
|
||||
|
||||
@@ -29,6 +29,7 @@ The code divides by concern. Each directory maps to the doc that owns its ration
|
||||
- `src/sources/` (`local-git`, `github-pr`) + `src/reporters/` (`terminal`, `github`) — where the diff comes from and where findings go [observed: AGENTS.md:14-15; src/sources/, src/reporters/]. Owned by [LLP 0008](0008-sources-and-reporters.explainer.md).
|
||||
- `templates/` + the scaffolded CI workflow — the files `ecr init` writes into an adopting repo; they define adopter-facing behavior and must stay in sync with the code [observed: AGENTS.md:19-20]. Owned by [LLP 0009](0009-adoption-templates-and-ci-workflows.guide.md).
|
||||
- `src/research-mcp/` + `research/` — the bundled read-only documentation MCP, scoped remote discovery and allowlisted fetch adapters, optional index builder, source catalog, and host-side evidence prepass. Owned by [LLP 0013](0013-platform-research.explainer.md).
|
||||
- `src/core/jev.ts` — optional typed first stage of finding verification against bounded local source. Owned by [LLP 0014](0014-jev-selective-verification.explainer.md).
|
||||
- The trust model spans all of the above and is stated as principles in [LLP 0001](0001-trust-model.principles.md).
|
||||
|
||||
## Cross-Cutting Invariants
|
||||
@@ -70,5 +71,6 @@ Start here, then follow the owning doc.
|
||||
| What do the CLI commands do, and how does CI orchestrate them? | [LLP 0007](0007-cli-commands-and-ci.explainer.md) | `src/cli.ts`, `src/commands/*` |
|
||||
| Where does the diff come from and where do findings go? | [LLP 0008](0008-sources-and-reporters.explainer.md) | `src/sources/*`, `src/reporters/*` |
|
||||
| What does `ecr init` scaffold into an adopting repo? | [LLP 0009](0009-adoption-templates-and-ci-workflows.guide.md) | `templates/`, scaffolded CI workflow |
|
||||
| How does Jev make narrow decisions and defer uncertain findings? | [LLP 0014](0014-jev-selective-verification.explainer.md) | `src/core/jev.ts`, `src/core/verify.ts` |
|
||||
|
||||
Non-doc sources of truth: `AGENTS.md` holds the working conventions (import style, error handling, the `opencode-ai`/`@opencode-ai/sdk` pinning rule, security invariants); `ROADMAP.md` lists open and deferred items — incremental review, inline PR comments, result-level caching, and a Codex token rotator among them are still open and must not be assumed implemented [observed: ROADMAP.md]; and `templates/` is the adopter-facing contract, so treat it as behavior, not sample text.
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
## Pipeline Stages
|
||||
|
||||
`runReview()` runs a fixed sequence: filter noise → resolve the read root (the trust boundary) → start the engines → preflight that every model id resolves → chunk the diff → run agent×chunk passes plus one cross-cutting pass through a growable bounded queue → coordinate → verify → suppress → reconcile the summary → log. The whole run's resource lifecycle — engine handles, auth material, cwd, and the read-root worktree — is released in a single `finally` (`review.ts:947-951`), so no partial-startup path leaks a server or a worktree.
|
||||
`runReview()` runs a fixed sequence: filter noise → resolve the read root (the trust boundary) → start the engines → preflight that every model id resolves → chunk the diff → run agent×chunk passes plus one cross-cutting pass through a growable bounded queue → coordinate → verify → suppress → reconcile the summary → log. Optional Jev classification is the first stage inside verification: clear local judgments act and uncertainty defers to the reasoning verifier. [LLP 0014](0014-jev-selective-verification.explainer.md) owns that boundary. The whole run's resource lifecycle — engine handles, auth material, cwd, and the read-root worktree — is released in a single `finally` (`review.ts:947-951`), so no partial-startup path leaks a server or a worktree.
|
||||
|
||||
Two ordering decisions inside that lifecycle are load-bearing and easy to break by "cleaning up":
|
||||
|
||||
@@ -77,7 +77,7 @@ Rate-limit evidence is tracked per engine separately and surfaced with the cause
|
||||
|
||||
`writeRunLog()` (`src/core/log.ts`) appends one JSON line per run — cost, tokens (run-wide and per bucket), findings, coverage notes, decision — so runs are auditable and cost/latency measurable later. The record deliberately excludes PR title and body text, keeping only `baseRef`/`headRef`, to avoid persisting secrets that might appear in author-controlled text [observed] (`log.ts:13-18`). Findings may quote changed source lines, but only content the review already publishes verbatim in the PR comment.
|
||||
|
||||
Each successful reviewer pass may also return a bounded conclusion-only trace: up to three concrete checks and two unresolved questions, with each note capped at 240 characters. Trace parsing is deliberately fail-soft: malformed optional diagnostics are dropped while valid findings from the same response survive, so observability can never create a coverage gap. `runReview()` merges and de-duplicates valid notes per reviewer, sorts reviewer ids, caps both the stored roster and the complete decoded trace (6 KB), labels the result `unverified-model-diagnostics`, and attaches it to `CoordinatorOutput.reviewTrace` only after the decision pipeline finishes. `attachReviewTrace` strips any coordinator-authored value before attaching the engine-built trace; even a malformed injected coordinator value parses as absent rather than failing consolidation. The trace therefore never reaches coordination, verification, suppression, or decision logic. Because the GitHub comment already embeds the full coordinator output as durable state, downstream agents can recover the trace without adding visible review prose or storing raw transcripts and chain-of-thought. The same trace rides the run log for short-lived operational inspection [observed: `schema.ts` `ReviewerTraceNotesSchema`/`ReviewTraceSchema`/`ReviewerOutputSchema`; `review.ts` `mergeTraceNotes`/`buildReviewTrace`/`attachReviewTrace`; `render.ts` embeds the full review].
|
||||
Each successful reviewer pass may also return a bounded conclusion-only trace: up to three concrete checks and two unresolved questions, with each note capped at 240 characters. Trace parsing is deliberately fail-soft: malformed optional diagnostics are dropped while valid findings from the same response survive, so observability can never create a coverage gap. `runReview()` merges and de-duplicates valid notes per reviewer, sorts reviewer ids, caps both the stored roster and the complete decoded trace (6 KB), labels the result `unverified-model-diagnostics`, and attaches it to `CoordinatorOutput.reviewTrace` only after the decision pipeline finishes. `attachReviewTrace` strips any coordinator-authored value before attaching the engine-built trace; even a malformed injected coordinator value parses as absent rather than failing consolidation. The trace therefore never reaches coordination, verification, suppression, or decision logic. Because the GitHub comment already embeds the full coordinator output as durable state, downstream agents can recover the trace without adding visible review prose or storing raw transcripts and chain-of-thought. The same trace rides the run log for short-lived operational inspection [observed: `schema.ts` `ReviewerTraceNotesSchema`/`ReviewTraceSchema`/`ReviewerOutputSchema`; `review.ts` `mergeTraceNotes`/`buildReviewTrace`/`attachReviewTrace`; `render.ts` embeds the full review]. Optional Jev aggregate usage is a second log sink; individual suppressions use the ordinary `verifierDropped` audit trail, and neither contains extra source text.
|
||||
|
||||
Observability must never break a review, so both output sinks swallow their own errors: `safeLog` wraps `writeRunLog` and discards any write error [observed] (`review.ts:1269-1275`), and `appendStepSummary` is a no-op outside Actions and swallows append failures [observed] (`step-summary.ts:9-19`). A consequence worth stating: a missing or corrupt run-log entry is therefore **not** an error signal — the log's presence or absence proves nothing about whether the review succeeded.
|
||||
|
||||
|
||||
@@ -109,12 +109,15 @@ the Responses API and preserve encrypted reasoning across tool turns. The generi
|
||||
Completions path loses that continuity [observed:
|
||||
`opencode.ts` `META_MODEL_API_BASE_URL` / `META_MUSE_MODELS` / `buildOpencodeConfig`].
|
||||
|
||||
The declared model catalog is an allowlist (`muse-spark-1.2` and
|
||||
`muse-spark-1.2-contributor`), not a reflection of arbitrary config strings. This keeps
|
||||
the existing preflight honest: a misspelled or not-yet-supported Meta model remains
|
||||
absent from the server's provider model list and fails once before any pass. Both models
|
||||
declare their public context/output limits, multimodal inputs, high reasoning, and
|
||||
encrypted reasoning inclusion [observed: `opencode.ts` `META_MUSE_MODELS`].
|
||||
Provider selection is forward-compatible at the provider boundary, not coupled to the
|
||||
current Muse family name: every referenced non-empty `meta/<model>` id is declared on
|
||||
the synthesized Meta provider. Known Muse Spark 1.2 and 1.3 standard/Contributor ids
|
||||
receive their published limits, multimodal inputs, high reasoning, and encrypted
|
||||
reasoning inclusion from `META_MUSE_MODELS`; a future id such as `meta/muse-foo` is
|
||||
declared with neutral metadata rather than guessed capabilities. This deliberately
|
||||
moves validation of an unknown-but-well-formed Meta id from OpenCode's local preflight
|
||||
to its first Meta request, in exchange for new Meta names working without an ECR
|
||||
release [observed: `opencode.ts` `META_MUSE_MODELS` / `buildOpencodeConfig`].
|
||||
|
||||
## Claude Code CLI Containment
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ caller, [LLP 0007](0007-cli-commands-and-ci.explainer.md) and [LLP 0001](0001-tr
|
||||
## Root vs Scope Config
|
||||
|
||||
A scope config is the root schema minus the centrally locked keys. `ScopeReviewConfigSchema`
|
||||
takes `ReviewConfigSchema`, omits `auth`, `breakGlass`, and `commentTag`, and re-adds each as
|
||||
takes `ReviewConfigSchema`, omits centrally owned keys including `auth`, `breakGlass`,
|
||||
`commentTag`, `research`, and `jev`, and re-adds each as
|
||||
`z.never().optional()` so declaring any of them is a hard parse error, not a later runtime
|
||||
check [observed: `schema.ts:232-247`]. The point of failing at the Zod level is that an IDE or
|
||||
`doctor` catches it before CI does [observed: `schema.ts:224-225` comment]. The remaining
|
||||
@@ -84,6 +85,13 @@ commit `43b31a4` follow-up]. The actual secret values never enter this subsystem
|
||||
owns the wiring and the `FORBIDDEN_TOKEN_ENVS` list, and schema/load only ever touch the env-var
|
||||
NAME [observed: `AGENTS.md` security invariants; `src/core/auth.ts:28,45,206`].
|
||||
|
||||
`jev` is root-only for the same trust reason as research: adding it sends bounded
|
||||
repository-derived text to another provider. A scope may neither activate it nor alter
|
||||
its model, threshold, or limits. Nested scopes inherit the root's resolved settings, so
|
||||
one PR has one centrally owned disclosure policy [observed: `schema.ts`
|
||||
`ScopeReviewConfigSchema`; `load.ts` `loadScopeConfig`]. The data boundary and active
|
||||
selective cascade live in [LLP 0014](0014-jev-selective-verification.explainer.md).
|
||||
|
||||
## Model Resolution
|
||||
|
||||
The `REVIEWER_MODEL` env override is applied on top of frontmatter/default model resolution using
|
||||
|
||||
@@ -56,7 +56,7 @@ The three workflows share a posture built entirely around one fact: a reviewer j
|
||||
**Concurrency defaults by auth mode.** The commented `chunk.concurrency` default is 6 for an API key and 3 for a subscription (OAuth) credential, "one account handles many parallel streams poorly, and several PRs may review on the same credential at once" [observed: `templates/config.jsonc:28-30`].
|
||||
|
||||
**Muse Spark is opt-in without weakening the default secret surface.** The root config
|
||||
documents `meta/muse-spark-1.2` plus a `META_API_KEY` auth entry, and each
|
||||
documents `meta/muse-spark-1.3` plus a `META_API_KEY` auth entry, and each
|
||||
review-running workflow shows the corresponding secret line commented out. It is not
|
||||
forwarded by the default scaffold: adopters replace the Anthropic line and set
|
||||
`ECR_EXPECTED_TOKEN_ENV=META_API_KEY`, so the model process receives only the
|
||||
@@ -64,6 +64,15 @@ credential actually selected by trusted config. The default remains Anthropic an
|
||||
`dismiss.yml` still receives no model credential [observed: `templates/config.jsonc`;
|
||||
`templates/workflow.yml`, `command.yml`, and `atlantis.yml`].
|
||||
|
||||
**Jev is an independent optional secret.** Each review-running workflow maps
|
||||
`TYPESAFE_API_KEY`; adding `jev` to trusted root config activates the selective cascade.
|
||||
GitHub supplies an empty value when the repository secret is absent, so the original
|
||||
verification path remains available. ECR sends bounded finding/file source directly
|
||||
to TypeSafe and logs aggregate usage; the key never enters `auth.tokenEnv` or reviewer
|
||||
engines. `dismiss.yml` still receives no model or Jev credential.
|
||||
[LLP 0014](0014-jev-selective-verification.explainer.md) owns the disclosure and
|
||||
fallback rules.
|
||||
|
||||
**Keep the `**/*` catch-all first in `routing.jsonc`.** Scopes match last-match-wins, so a `**/*` catch-all must come first and more specific scopes come after to override it [observed: `templates/routing.jsonc:27-31`; `AGENTS.md:17-18`]. Nothing validates the order — reversing it silently changes which config applies to already-routed files [inferred: no code checks that the `**/*` catch-all comes first, so reversing it silently changes routing].
|
||||
|
||||
**`scope-config.jsonc` deliberately omits `auth` and `commentTag`.** It is a *different* template from the root `config.jsonc`, not a copy. A scope config carries no `auth` block — credentials are root-only, and the loader plus the CI guard reject a scope config that declares one [observed: `templates/scope-config.jsonc:1-3`; `AGENTS.md:51`]. It also omits `commentTag`, because a scope's PR-comment marker is always derived as `<rootTag>:<scope-name>` so `ecr ci` and `ecr review --scope --post` target the same comment; declaring one is rejected by the scope schema [observed: `templates/scope-config.jsonc:23-25`]. Reusing the root `config.jsonc` template for a scope — a tempting "simplification" — would break the trust boundary (LLP 0006).
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
# LLP 0014: Jev Selective Verification
|
||||
|
||||
**Type:** Explainer
|
||||
**Status:** Active
|
||||
**Systems:** Engine, Config, Security, Observability, Templates
|
||||
**Author:** Expo
|
||||
**Date:** 2026-09-21
|
||||
**Related:** [LLP 0001](0001-trust-model.principles.md), [LLP 0002](0002-review-engine-pipeline.explainer.md), [LLP 0005](0005-verification-fingerprints-rendering.explainer.md), [LLP 0006](0006-config-schema-loading-routing.explainer.md), [LLP 0009](0009-adoption-templates-and-ci-workflows.guide.md)
|
||||
|
||||
Jev is a discriminative model from TypeSafe AI. It answers typed questions over
|
||||
provided state and returns a probability distribution; it does not browse a repo,
|
||||
discover open-ended defects, explain causality, or write remediation. ECR therefore
|
||||
uses Jev inside verification, after generative reviewers have produced findings.
|
||||
|
||||
## Decision
|
||||
|
||||
ECR uses an active `generate → classify → defer` cascade. Jev is neither a shadow
|
||||
observer nor another general reviewer agent:
|
||||
|
||||
1. generative reviewers discover and explain candidate defects;
|
||||
2. deterministic code confines paths and grades quoted evidence;
|
||||
3. Jev makes one atomic local support judgment per finding;
|
||||
4. high-confidence support keeps the finding, while high-confidence contradiction
|
||||
drops only ordinary correctness/quality findings;
|
||||
5. uncertainty and protected findings defer to the existing reasoning verifier.
|
||||
|
||||
Adding the root `jev` config activates the cascade. There is no separate `enabled`
|
||||
flag and no shadow phase. If the config, credential, or a usable response is absent,
|
||||
the finding follows the pre-Jev verification rules.
|
||||
|
||||
## Evidence Survey
|
||||
|
||||
The primary implementation sources are TypeSafe's [Jev announcement](https://typesafe.ai/blog/introducing-system-one-models-and-jev),
|
||||
[System One guide](https://docs.typesafe.ai/concepts/how-to-build-with-system-one),
|
||||
[confidence guidance](https://docs.typesafe.ai/confidence),
|
||||
[confidence-routing pattern](https://docs.typesafe.ai/patterns/confidence-routing),
|
||||
[citation-check cookbook](https://docs.typesafe.ai/cookbooks/citation_check),
|
||||
[software-development cascade](https://docs.typesafe.ai/cookbooks/sde_cascade),
|
||||
[Jev 1.13 model notes](https://docs.typesafe.ai/model-jaggedness/jev-1.13), and the
|
||||
open-source [JavaScript SDK](https://github.com/typesafe-ai/typesafe-sdk-js).
|
||||
|
||||
The consistent vendor guidance is to keep deterministic control flow in code, ask
|
||||
narrow independent questions over relevant state, act only above a risk-appropriate
|
||||
confidence threshold, and route uncertain results to a stronger reasoner. Confidence
|
||||
measures concentration of the answer distribution, not factual correctness. Jev is
|
||||
also documented as weak on arithmetic, counting, dates, complex indirection, and
|
||||
irrelevant context. These limits rule out using it as the only verifier.
|
||||
|
||||
The architecture also follows older work on selective prediction and abstention:
|
||||
[SelectiveNet](https://proceedings.mlr.press/v97/geifman19a),
|
||||
[Learning to Defer](https://proceedings.mlr.press/v119/mozannar20b.html),
|
||||
[Calibrated Learning to Defer](https://proceedings.mlr.press/v162/verma22c.html), and
|
||||
[Language Model Cascades](https://arxiv.org/abs/2207.10342). Their shared idea is
|
||||
that a useful classifier should act on its competence region and reject or defer the
|
||||
rest. [Generative Verifiers](https://openreview.net/forum?id=Ccwp4tFEtE) further
|
||||
supports retaining a generative verifier for multi-step reasoning rather than treating
|
||||
a discriminative score as universal proof.
|
||||
|
||||
Code-review-specific evidence argues for conservative authority. Research on
|
||||
[LLM code-review overcorrection](https://arxiv.org/abs/2603.00539),
|
||||
[security code review](https://arxiv.org/abs/2401.16310), and
|
||||
[judge calibration and bias](https://aclanthology.org/2025.acl-long.808/) shows that
|
||||
binary judgments can be confidently wrong and that security claims need stronger
|
||||
grounding. Vendor benchmarks measure agreement with other model outputs rather than
|
||||
human-adjudicated ECR findings, so they establish speed and structured behavior, not
|
||||
an acceptable false-negative rate for this repository.
|
||||
|
||||
The useful philosophical analogy is dual-process reasoning: Jev supplies a fast,
|
||||
bounded System 1 judgment; the tool-using verifier supplies slower System 2 analysis.
|
||||
The relevant engineering principle is a reject option, not majority voting. Jev's
|
||||
different model family adds diversity, but agreement between two models is not ground
|
||||
truth and disagreement is a reason to inspect, not to average blindly.
|
||||
|
||||
## Active Selective Cascade
|
||||
|
||||
The policy is intentionally asymmetric:
|
||||
|
||||
| Jev result | Ordinary correctness/quality | Critical, security, secrets, or cited |
|
||||
| --- | --- | --- |
|
||||
| supported at or above threshold | keep | cited findings defer; other protected findings keep |
|
||||
| contradicted at or above threshold | drop with an audit reason | defer to reasoning verifier |
|
||||
| `needs_reasoning` or low confidence | defer to reasoning verifier | defer to reasoning verifier |
|
||||
| missing response or provider failure | use original verification path | use original verification path |
|
||||
|
||||
“Protected” controls the dangerous direction: Jev may help retain such a finding, but
|
||||
cannot suppress it alone. Cited findings always defer because Jev receives repository
|
||||
source, not the audited external passage whose support must be checked.
|
||||
|
||||
The default `minConfidence` is `0.9`. It is a conservative operating point, not a
|
||||
claim of 90% correctness. Repositories may change it explicitly, but model version,
|
||||
question text, state shape, threshold, and truncation policy form one classifier and
|
||||
must be reviewed together. A dated model is preferred over a moving alias.
|
||||
|
||||
## State and Security Boundaries
|
||||
|
||||
Each request includes finding category, severity, file, line, title, rationale, and
|
||||
quoted evidence plus bounded source from that file. ECR sends no PR title/body,
|
||||
unrelated file, agent transcript, author reply, or credential. `maxContextChars`
|
||||
bounds disclosure and context rot; truncation centers on the quoted evidence where
|
||||
possible. Four requests run concurrently, with a bounded timeout and one retry.
|
||||
|
||||
The API origin is explicit. Ambient TypeSafe URL/model variables cannot redirect the
|
||||
request. SDK logging is off because bodies contain source. `TYPESAFE_API_KEY` is
|
||||
captured before reviewer startup, withheld from OpenCode, and forbidden as a reviewer
|
||||
provider credential. Paths remain confined to the materialized review tree before
|
||||
any source is read.
|
||||
|
||||
Jev is not a prompt-injection detector or security boundary. The code and finding
|
||||
text are attacker-controlled inputs, and model output is schema-validated before the
|
||||
policy sees it. Deterministic code continues to own credential isolation, path
|
||||
confinement, evidence matching, category floors, and final decision derivation.
|
||||
|
||||
## Failure and Observability
|
||||
|
||||
The cascade fails back, not closed. Missing credentials disable only Jev for that run.
|
||||
An individual timeout, malformed response, or provider error sends that finding
|
||||
through the original verification path. Error text is not persisted because providers
|
||||
may echo source. The run log stores aggregate model, duration, token, cost, evaluated,
|
||||
and failure counts; any finding dropped by Jev appears in the ordinary
|
||||
`verifierDropped` audit trail with its confidence.
|
||||
|
||||
Published pricing for the pinned integration is $0.042 per million input tokens with
|
||||
no output-token charge. Cost is recorded in the `jev-verifier` bucket. Provider limits
|
||||
are not treated as local capacity guarantees.
|
||||
|
||||
## What Jev Replaces
|
||||
|
||||
Jev replaces only straightforward finding verification calls. It does not replace:
|
||||
|
||||
- reviewer agents that discover bugs and supply explanations;
|
||||
- the coordinator that merges and prioritizes findings;
|
||||
- deterministic evidence, trust, and security checks;
|
||||
- the reasoning verifier for ambiguity, multi-file behavior, runtime semantics,
|
||||
citations, or protected negative judgments.
|
||||
|
||||
This division gives Jev real authority and cost benefit without pretending that a
|
||||
fast typed classifier is a complete code reviewer.
|
||||
|
||||
## Rejected Alternatives
|
||||
|
||||
**A standalone `jev.md` reviewer agent.** Jev cannot use ECR's browsing tools or emit
|
||||
the finding schema's explanation and remediation. Presenting it as a peer reviewer
|
||||
would hide a different and narrower capability behind the same abstraction.
|
||||
|
||||
**Shadow-only operation.** It adds latency and cost without improving the review. The
|
||||
safe boundary comes from abstention, protected categories, and fallback behavior, not
|
||||
from making every result inert.
|
||||
|
||||
**Replace all verification.** Local classification cannot resolve missing repository
|
||||
context, multi-step execution, arithmetic, temporal behavior, or external citations.
|
||||
|
||||
**Use confidence as proof.** A concentrated distribution can still be wrong. The
|
||||
threshold is a routing policy and protected contradictions still require reasoning.
|
||||
|
||||
**Send the entire diff or repository.** Unrelated context increases disclosure and
|
||||
context rot. One bounded file is the smallest state that supports the local question.
|
||||
@@ -45,6 +45,7 @@
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "1.30.0",
|
||||
"@opencode-ai/sdk": "1.18.4",
|
||||
"@typesafe-ai/sdk": "0.6.0",
|
||||
"cheerio": "1.1.2",
|
||||
"minisearch": "7.2.0",
|
||||
"opencode-ai": "1.18.4",
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { TypeSafeClient } from "@typesafe-ai/sdk";
|
||||
|
||||
import { ReviewConfigSchema, ScopeReviewConfigSchema } from "../config/schema.js";
|
||||
import { FORBIDDEN_TOKEN_ENVS } from "../core/auth.js";
|
||||
import {
|
||||
buildJevState,
|
||||
evaluateFindingsWithJev,
|
||||
jevDisposition,
|
||||
withoutJevCredential,
|
||||
} from "../core/jev.js";
|
||||
import type { Finding } from "../core/schema.js";
|
||||
|
||||
const finding: Finding = {
|
||||
severity: "warning",
|
||||
category: "correctness",
|
||||
file: "src/example.ts",
|
||||
line: 2,
|
||||
title: "The fallback returns the wrong value",
|
||||
rationale: "The changed branch returns false when the caller requires true.",
|
||||
evidence: "return false",
|
||||
};
|
||||
|
||||
const config = {
|
||||
model: "jev-1.13.0",
|
||||
minConfidence: 0.9,
|
||||
timeoutMs: 10_000,
|
||||
maxContextChars: 30_000,
|
||||
};
|
||||
|
||||
describe("Jev config", () => {
|
||||
test("is activated by presence and absent by default", () => {
|
||||
expect(ReviewConfigSchema.parse({}).jev).toBeUndefined();
|
||||
expect(ReviewConfigSchema.parse({ jev: {} }).jev).toEqual(config);
|
||||
});
|
||||
|
||||
test("is root-only", () => {
|
||||
expect(ScopeReviewConfigSchema.safeParse({ jev: {} }).success).toBe(false);
|
||||
});
|
||||
|
||||
test("cannot be repurposed as a reviewer provider credential", () => {
|
||||
expect(FORBIDDEN_TOKEN_ENVS.has("TYPESAFE_API_KEY")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("evaluateFindingsWithJev", () => {
|
||||
test("falls back cleanly when the key is missing", async () => {
|
||||
const result = await evaluateFindingsWithJev({
|
||||
config,
|
||||
candidates: [{ finding, sourceContext: "return false" }],
|
||||
env: {},
|
||||
});
|
||||
expect(result.summary.unavailable).toBe("TYPESAFE_API_KEY is not set");
|
||||
expect(result.evaluations.size).toBe(0);
|
||||
});
|
||||
|
||||
test("returns a typed atomic judgment without persisting source", async () => {
|
||||
const requests: unknown[] = [];
|
||||
const client = new TypeSafeClient({
|
||||
apiKey: "test-key",
|
||||
defaultModel: config.model,
|
||||
retry: { maxRetries: 0 },
|
||||
fetch: async (_input, init) => {
|
||||
requests.push(JSON.parse(String(init?.body)));
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
model: "jev-1.13.0",
|
||||
answers: {
|
||||
support: {
|
||||
type: "choice",
|
||||
choice: "supported",
|
||||
confidence: 0.91,
|
||||
probabilities: { supported: 0.91, needs_reasoning: 0.07, contradicted: 0.02 },
|
||||
},
|
||||
},
|
||||
usage: { input_tokens: 123, output_tokens: 0 },
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const result = await evaluateFindingsWithJev({
|
||||
config,
|
||||
candidates: [{ finding, sourceContext: "export function fallback() { return false }" }],
|
||||
client,
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(requests).toHaveLength(1);
|
||||
expect(result.summary.evaluated).toBe(1);
|
||||
expect(result.summary.inputTokens).toBe(123);
|
||||
expect([...result.evaluations.values()][0]?.support).toBe("supported");
|
||||
expect(JSON.stringify(result.summary)).not.toContain("return false");
|
||||
});
|
||||
|
||||
test("fails open per finding on provider errors", async () => {
|
||||
const client = new TypeSafeClient({
|
||||
apiKey: "test-key",
|
||||
retry: { maxRetries: 0 },
|
||||
fetch: async () => {
|
||||
throw new Error("request contained sensitive source");
|
||||
},
|
||||
});
|
||||
const result = await evaluateFindingsWithJev({
|
||||
config,
|
||||
candidates: [{ finding, sourceContext: "secret source" }],
|
||||
client,
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(result.summary.failed).toBe(1);
|
||||
expect(result.summary.unavailable).toBeUndefined();
|
||||
expect(JSON.stringify(result.summary)).not.toContain("sensitive source");
|
||||
});
|
||||
});
|
||||
|
||||
describe("jevDisposition", () => {
|
||||
const evaluation = (
|
||||
support: "supported" | "needs_reasoning" | "contradicted",
|
||||
confidence = 0.95,
|
||||
) => ({
|
||||
fingerprint: "fp",
|
||||
model: config.model,
|
||||
support,
|
||||
confidence,
|
||||
probabilities: { supported: 0.02, needs_reasoning: 0.03, contradicted: 0.95 },
|
||||
});
|
||||
|
||||
test("acts on clear ordinary findings and defers uncertainty", () => {
|
||||
expect(jevDisposition(finding, evaluation("supported"), 0.9)).toBe("keep");
|
||||
expect(jevDisposition(finding, evaluation("contradicted"), 0.9)).toBe("drop");
|
||||
expect(jevDisposition(finding, evaluation("needs_reasoning"), 0.9)).toBe("defer");
|
||||
expect(jevDisposition(finding, evaluation("contradicted", 0.89), 0.9)).toBe("defer");
|
||||
});
|
||||
|
||||
test("never drops protected or cited findings by itself", () => {
|
||||
expect(
|
||||
jevDisposition({ ...finding, category: "security" }, evaluation("contradicted"), 0.9),
|
||||
).toBe("defer");
|
||||
expect(
|
||||
jevDisposition({ ...finding, severity: "critical" }, evaluation("contradicted"), 0.9),
|
||||
).toBe("defer");
|
||||
expect(
|
||||
jevDisposition(
|
||||
{ ...finding, sources: [{ title: "Docs", url: "https://example.com/docs" }] },
|
||||
evaluation("supported"),
|
||||
0.9,
|
||||
),
|
||||
).toBe("defer");
|
||||
});
|
||||
});
|
||||
|
||||
test("buildJevState bounds large source around the cited evidence", () => {
|
||||
const state = buildJevState(
|
||||
finding,
|
||||
`before-${"x".repeat(2_000)}return false${"y".repeat(2_000)}`,
|
||||
1_000,
|
||||
);
|
||||
expect(state.sourceContext.length).toBeLessThanOrEqual(1_000);
|
||||
expect(state.sourceContext).toContain("return false");
|
||||
expect(state.sourceContext).toContain("source omitted by ecr");
|
||||
});
|
||||
|
||||
test("withoutJevCredential restores the key after hiding it from a child spawn", async () => {
|
||||
const env = { TYPESAFE_API_KEY: "secret", KEEP: "visible" };
|
||||
const seen = await withoutJevCredential(async () => ({ ...env }), env);
|
||||
expect(seen).toEqual({ KEEP: "visible" });
|
||||
expect(env.TYPESAFE_API_KEY).toBe("secret");
|
||||
});
|
||||
@@ -342,10 +342,10 @@ test("an upstream-alias auth entry synthesizes a provider block with exactly the
|
||||
expect(opencode.provider?.openai).toBeUndefined();
|
||||
});
|
||||
|
||||
test("Meta auth synthesizes the public Muse Responses provider without embedding its key", () => {
|
||||
test("Meta auth synthesizes the public Muse 1.3 Responses provider without embedding its key", () => {
|
||||
const config = configWith({
|
||||
agents: [{ id: "correctness", model: "meta/muse-spark-1.2" }],
|
||||
coordinatorModel: "meta/muse-spark-1.2",
|
||||
agents: [{ id: "correctness", model: "meta/muse-spark-1.3" }],
|
||||
coordinatorModel: "meta/muse-spark-1.3-contributor",
|
||||
auth: [{ mode: "api-key", provider: "meta", tokenEnv: "META_API_KEY" }],
|
||||
});
|
||||
const opencode = buildOpencodeConfig(config) as {
|
||||
@@ -354,7 +354,14 @@ test("Meta auth synthesizes the public Muse Responses provider without embedding
|
||||
{
|
||||
npm: string;
|
||||
options: { baseURL: string; apiKey: string };
|
||||
models: Record<string, { reasoning?: boolean; options?: Record<string, unknown> }>;
|
||||
models: Record<
|
||||
string,
|
||||
{
|
||||
reasoning?: boolean;
|
||||
limit?: { context: number; output: number };
|
||||
options?: Record<string, unknown>;
|
||||
}
|
||||
>;
|
||||
}
|
||||
>;
|
||||
};
|
||||
@@ -365,12 +372,17 @@ test("Meta auth synthesizes the public Muse Responses provider without embedding
|
||||
baseURL: META_MODEL_API_BASE_URL,
|
||||
apiKey: "{env:META_API_KEY}",
|
||||
});
|
||||
expect(Object.keys(meta!.models)).toEqual(["muse-spark-1.2"]);
|
||||
expect(meta!.models["muse-spark-1.2"]?.reasoning).toBe(true);
|
||||
expect(meta!.models["muse-spark-1.2"]?.options).toMatchObject({
|
||||
expect(Object.keys(meta!.models)).toEqual(["muse-spark-1.3", "muse-spark-1.3-contributor"]);
|
||||
expect(meta!.models["muse-spark-1.3"]?.reasoning).toBe(true);
|
||||
expect(meta!.models["muse-spark-1.3"]?.limit).toEqual({
|
||||
context: 1_048_576,
|
||||
output: 1_048_576,
|
||||
});
|
||||
expect(meta!.models["muse-spark-1.3"]?.options).toMatchObject({
|
||||
reasoningEffort: "high",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
});
|
||||
expect(meta!.models["muse-spark-1.3-contributor"]?.reasoning).toBe(true);
|
||||
});
|
||||
|
||||
test("a Meta-named upstream alias does not redirect the upstream credential to Meta", () => {
|
||||
@@ -400,25 +412,22 @@ test("a Meta-named upstream alias does not redirect the upstream credential to M
|
||||
expect(Object.keys(alias!.models)).toEqual(["muse-spark-1.2"]);
|
||||
});
|
||||
|
||||
test("Meta provider does not register an unknown Muse id, so preflight can reject it", () => {
|
||||
test("Meta provider registers future model families neutrally and only for the meta prefix", () => {
|
||||
const config = configWith({
|
||||
agents: [{ id: "correctness", model: "meta/muse-spark-typo" }],
|
||||
coordinatorModel: "meta/muse-spark-typo",
|
||||
agents: [
|
||||
{ id: "correctness", model: "meta/muse-foo" },
|
||||
{ id: "security", model: "openai/muse-foo" },
|
||||
],
|
||||
coordinatorModel: "meta/a-future-family-2",
|
||||
auth: [{ mode: "api-key", provider: "meta", tokenEnv: "META_API_KEY" }],
|
||||
});
|
||||
const opencode = buildOpencodeConfig(config) as {
|
||||
provider?: Record<string, { models: Record<string, unknown> }>;
|
||||
};
|
||||
expect(opencode.provider?.meta?.models).toEqual({});
|
||||
expect(
|
||||
findUnknownModels(["meta/muse-spark-typo"], { meta: ["muse-spark-1.2"] }, ["meta"]),
|
||||
).toEqual([
|
||||
{
|
||||
model: "meta/muse-spark-typo",
|
||||
reason: "model",
|
||||
suggestions: ["muse-spark-1.2"],
|
||||
},
|
||||
]);
|
||||
expect(opencode.provider?.meta?.models).toEqual({
|
||||
"muse-foo": {},
|
||||
"a-future-family-2": {},
|
||||
});
|
||||
});
|
||||
|
||||
test("buildOpencodeConfig registers the no-tools stack verifier (empty tool list)", () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect, beforeAll, afterAll } from "bun:test";
|
||||
import { TypeSafeClient } from "@typesafe-ai/sdk";
|
||||
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
@@ -82,6 +83,34 @@ const finding = (over: Partial<Finding>): Finding => ({
|
||||
// (keep the finding). Grounded non-criticals never reach the LLM at all.
|
||||
const handle = {} as OpencodeHandle;
|
||||
|
||||
const contradictedJev = new TypeSafeClient({
|
||||
apiKey: "test-key",
|
||||
retry: { maxRetries: 0 },
|
||||
fetch: async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
model: "jev-1.13.0",
|
||||
answers: {
|
||||
support: {
|
||||
type: "choice",
|
||||
choice: "contradicted",
|
||||
confidence: 0.97,
|
||||
probabilities: { supported: 0.01, needs_reasoning: 0.02, contradicted: 0.97 },
|
||||
},
|
||||
},
|
||||
usage: { input_tokens: 20, output_tokens: 0 },
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
),
|
||||
});
|
||||
|
||||
const jevConfig = {
|
||||
model: "jev-1.13.0",
|
||||
minConfidence: 0.9,
|
||||
timeoutMs: 10_000,
|
||||
maxContextChars: 30_000,
|
||||
};
|
||||
|
||||
test("grounded non-critical is kept without an LLM call", async () => {
|
||||
const res = await verifyFindings(
|
||||
handle,
|
||||
@@ -92,6 +121,40 @@ test("grounded non-critical is kept without an LLM call", async () => {
|
||||
expect(res.dropped).toEqual([]);
|
||||
});
|
||||
|
||||
test("Jev actively drops a confidently contradicted ordinary finding", async () => {
|
||||
const res = await verifyFindings(
|
||||
handle,
|
||||
[finding({ title: "contradicted", evidence: "return items[next++]!;" })],
|
||||
"/",
|
||||
undefined,
|
||||
[],
|
||||
{ config: jevConfig, client: contradictedJev },
|
||||
);
|
||||
expect(res.kept).toEqual([]);
|
||||
expect(res.dropped[0]?.reason).toContain("Jev directly contradicted");
|
||||
expect(res.jev?.evaluated).toBe(1);
|
||||
});
|
||||
|
||||
test("Jev defers a contradicted security finding to the reasoning verifier", async () => {
|
||||
const res = await verifyFindings(
|
||||
handle,
|
||||
[
|
||||
finding({
|
||||
title: "protected",
|
||||
category: "security",
|
||||
evidence: "return items[next++]!;",
|
||||
}),
|
||||
],
|
||||
"/",
|
||||
undefined,
|
||||
[],
|
||||
{ config: jevConfig, client: contradictedJev },
|
||||
);
|
||||
// The dummy reasoning handle fails, and the existing fail-open rule keeps it.
|
||||
expect(res.kept.map((item) => item.title)).toEqual(["protected"]);
|
||||
expect(res.dropped).toEqual([]);
|
||||
});
|
||||
|
||||
test("absent evidence is ESCALATED, not hard-dropped (fail-open keeps it here)", async () => {
|
||||
// The key regression guard: an imperfect/absent quote must not be a silent drop.
|
||||
// With the dummy handle the escalated LLM call throws → fail-open → kept.
|
||||
|
||||
+4
-1
@@ -112,7 +112,7 @@ export interface LoadConfigOptions {
|
||||
/** Parsed config with the centrally-locked keys optional (scope configs omit them). */
|
||||
type ParsedConfig = Omit<
|
||||
RawReviewConfig,
|
||||
"auth" | "breakGlass" | "commentTag" | "stack" | "feedback" | "research" | "inline"
|
||||
"auth" | "breakGlass" | "commentTag" | "stack" | "feedback" | "research" | "jev" | "inline"
|
||||
> & {
|
||||
auth?: RawReviewConfig["auth"];
|
||||
breakGlass?: RawReviewConfig["breakGlass"];
|
||||
@@ -120,6 +120,7 @@ type ParsedConfig = Omit<
|
||||
stack?: RawReviewConfig["stack"];
|
||||
feedback?: RawReviewConfig["feedback"];
|
||||
research?: RawReviewConfig["research"];
|
||||
jev?: RawReviewConfig["jev"];
|
||||
inline?: RawReviewConfig["inline"];
|
||||
};
|
||||
|
||||
@@ -240,6 +241,7 @@ async function loadConfigDir(
|
||||
// Root-only: scope schemas reject research configuration, so an untrusted
|
||||
// subtree cannot select the index or alter the network-facing runtime.
|
||||
research: parsed.research ?? RESEARCH_CONFIG_DEFAULTS,
|
||||
...(parsed.jev ? { jev: parsed.jev } : {}),
|
||||
// parsed.breakGlass/auth are always present for the root schema (defaults) and
|
||||
// absent for the scope schema; loadScopeConfig overrides both afterwards.
|
||||
breakGlassMarker: parsed.breakGlass?.marker ?? "/skip-review",
|
||||
@@ -427,6 +429,7 @@ export async function loadScopeConfig(
|
||||
feedback: rootConfig.feedback,
|
||||
inline: rootConfig.inline,
|
||||
research: rootConfig.research,
|
||||
jev: rootConfig.jev,
|
||||
scopeName: scope.name,
|
||||
};
|
||||
}
|
||||
|
||||
+28
-1
@@ -87,6 +87,19 @@ export const ReviewConfigSchema = z.object({
|
||||
resultsPerQuery: 2,
|
||||
timeoutMs: 30_000,
|
||||
}),
|
||||
// Optional Jev verification cascade. Presence enables it: confident local
|
||||
// judgments act immediately, while ambiguous or protected findings defer to
|
||||
// the reasoning verifier. ROOT-ONLY because it sends bounded source context
|
||||
// to a separate provider.
|
||||
// @ref LLP 0014#active-selective-cascade [implements] — narrow decisions act; uncertainty defers
|
||||
jev: z
|
||||
.object({
|
||||
model: z.string().min(1).default("jev-1.13.0"),
|
||||
minConfidence: z.number().min(0.5).max(1).default(0.9),
|
||||
timeoutMs: z.number().int().min(1000).max(60_000).default(10_000),
|
||||
maxContextChars: z.number().int().min(1000).max(30_000).default(30_000),
|
||||
})
|
||||
.optional(),
|
||||
breakGlass: z
|
||||
.object({ marker: z.string().default("/skip-review") })
|
||||
.default({ marker: "/skip-review" }),
|
||||
@@ -334,7 +347,7 @@ export type RoutingDefaults = RoutingManifest["defaults"];
|
||||
* Scope config = root config MINUS the centrally locked keys. Allowlist of
|
||||
* scope-overridable keys (Turborepo-style, graft 6): model, policy, chunk,
|
||||
* noise (+ the prompt files living beside it: shared.md, coordinator.md,
|
||||
* agents/). NEVER auth, breakGlass, or research — declaring one fails parsing at the
|
||||
* agents/). NEVER auth, breakGlass, research, or jev — declaring one fails parsing at the
|
||||
* Zod level so IDE/doctor catch it before CI. commentTag is also locked: a
|
||||
* scope's comment marker is always DERIVED (`<rootTag>:<scope>`; the default
|
||||
* scope keeps the root tag) so `ecr ci`'s post/clear/reconcile paths and a
|
||||
@@ -349,6 +362,7 @@ export const ScopeReviewConfigSchema = ReviewConfigSchema.omit({
|
||||
stack: true,
|
||||
feedback: true,
|
||||
research: true,
|
||||
jev: true,
|
||||
inline: true,
|
||||
}).extend({
|
||||
auth: z
|
||||
@@ -379,6 +393,12 @@ export const ScopeReviewConfigSchema = ReviewConfigSchema.omit({
|
||||
"research is locked to the root config because it starts a trusted host process; remove it from this scope config",
|
||||
})
|
||||
.optional(),
|
||||
jev: z
|
||||
.never({
|
||||
error:
|
||||
"jev is locked to the root config because it sends bounded source context to an external provider; remove it from this scope config",
|
||||
})
|
||||
.optional(),
|
||||
inline: z
|
||||
.never({
|
||||
error:
|
||||
@@ -462,6 +482,13 @@ export interface LoadedConfig {
|
||||
resultsPerQuery: number;
|
||||
timeoutMs: number;
|
||||
};
|
||||
/** Root-only Jev selective-verification settings; absent means disabled. */
|
||||
jev?: {
|
||||
model: string;
|
||||
minConfidence: number;
|
||||
timeoutMs: number;
|
||||
maxContextChars: number;
|
||||
};
|
||||
breakGlassMarker: string;
|
||||
commentTag: string;
|
||||
/** Every configured provider credential (one entry for the legacy single shape). */
|
||||
|
||||
@@ -50,6 +50,7 @@ export const FORBIDDEN_TOKEN_ENVS = new Set([
|
||||
"GITHUB_TOKEN",
|
||||
"GH_TOKEN",
|
||||
"BRAVE_SEARCH_API_KEY",
|
||||
"TYPESAFE_API_KEY",
|
||||
"ACTIONS_RUNTIME_TOKEN",
|
||||
"ACTIONS_ID_TOKEN_REQUEST_TOKEN",
|
||||
"AWS_ACCESS_KEY_ID",
|
||||
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
// @ref LLP 0014#active-selective-cascade [implements] — Jev handles narrow judgments and defers uncertainty
|
||||
import { TypeSafeClient, choice } from "@typesafe-ai/sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { LoadedConfig } from "../config/schema.js";
|
||||
import type { Finding } from "./schema.js";
|
||||
import { fingerprintFinding } from "./schema.js";
|
||||
|
||||
const TYPESAFE_API_URL = "https://api.typesafe.ai";
|
||||
const CONCURRENCY = 4;
|
||||
const MAX_FILE_CHARS = 1_000;
|
||||
const MAX_TITLE_CHARS = 1_000;
|
||||
const MAX_RATIONALE_CHARS = 6_000;
|
||||
const MAX_EVIDENCE_CHARS = 6_000;
|
||||
const INPUT_COST_PER_MILLION = 0.042;
|
||||
|
||||
const SUPPORT_CRITERIA = {
|
||||
supported: "The supplied source context directly demonstrates the exact reported problem.",
|
||||
needs_reasoning:
|
||||
"The context is insufficient, or the claim needs multi-file, runtime, arithmetic, temporal, or indirect reasoning.",
|
||||
contradicted:
|
||||
"The supplied source context directly demonstrates that the reported problem is not present.",
|
||||
} as const;
|
||||
|
||||
const probability = z.number().min(0).max(1);
|
||||
const JevResponseSchema = z.object({
|
||||
model: z.string().min(1),
|
||||
answers: z.object({
|
||||
support: z.object({
|
||||
type: z.literal("choice"),
|
||||
choice: z.enum(["supported", "needs_reasoning", "contradicted"]),
|
||||
confidence: probability,
|
||||
probabilities: z.object({
|
||||
supported: probability,
|
||||
needs_reasoning: probability,
|
||||
contradicted: probability,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
usage: z.object({
|
||||
input_tokens: z.number().int().nonnegative(),
|
||||
output_tokens: z.number().int().nonnegative(),
|
||||
}),
|
||||
});
|
||||
|
||||
export interface JevCandidate {
|
||||
finding: Finding;
|
||||
sourceContext: string;
|
||||
}
|
||||
|
||||
export interface JevEvaluation {
|
||||
fingerprint: string;
|
||||
model: string;
|
||||
support: keyof typeof SUPPORT_CRITERIA;
|
||||
confidence: number;
|
||||
probabilities: Record<keyof typeof SUPPORT_CRITERIA, number>;
|
||||
}
|
||||
|
||||
export interface JevVerificationSummary {
|
||||
configuredModel: string;
|
||||
actualModel?: string;
|
||||
durationMs: number;
|
||||
evaluated: number;
|
||||
failed: number;
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
cost: number;
|
||||
unavailable?: string;
|
||||
}
|
||||
|
||||
export interface JevVerificationResult {
|
||||
evaluations: Map<string, JevEvaluation>;
|
||||
summary: JevVerificationSummary;
|
||||
}
|
||||
|
||||
export interface EvaluateWithJevOptions {
|
||||
config: NonNullable<LoadedConfig["jev"]>;
|
||||
candidates: readonly JevCandidate[];
|
||||
env?: NodeJS.ProcessEnv;
|
||||
apiKey?: string;
|
||||
client?: TypeSafeClient;
|
||||
}
|
||||
|
||||
/** Run a reviewer-engine spawn without exposing the unrelated Jev credential. */
|
||||
export async function withoutJevCredential<T>(
|
||||
run: () => Promise<T>,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): Promise<T> {
|
||||
const apiKey = env.TYPESAFE_API_KEY;
|
||||
try {
|
||||
delete env.TYPESAFE_API_KEY;
|
||||
return await run();
|
||||
} finally {
|
||||
if (apiKey === undefined) delete env.TYPESAFE_API_KEY;
|
||||
else env.TYPESAFE_API_KEY = apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
/** Classify candidates with one atomic question; failures become missing evaluations. */
|
||||
export async function evaluateFindingsWithJev({
|
||||
config,
|
||||
candidates,
|
||||
env = process.env,
|
||||
apiKey: explicitApiKey,
|
||||
client,
|
||||
}: EvaluateWithJevOptions): Promise<JevVerificationResult> {
|
||||
const started = Date.now();
|
||||
const apiKey = (explicitApiKey ?? env.TYPESAFE_API_KEY)?.trim();
|
||||
if (!client && !apiKey) return emptyResult(config, started, "TYPESAFE_API_KEY is not set");
|
||||
|
||||
let resolvedClient: TypeSafeClient;
|
||||
try {
|
||||
resolvedClient =
|
||||
client ??
|
||||
new TypeSafeClient({
|
||||
apiKey,
|
||||
baseURL: TYPESAFE_API_URL,
|
||||
defaultModel: config.model,
|
||||
timeout: config.timeoutMs,
|
||||
retry: { maxRetries: 1 },
|
||||
logLevel: "off",
|
||||
});
|
||||
} catch {
|
||||
return emptyResult(config, started, "Jev client could not be initialized");
|
||||
}
|
||||
|
||||
const evaluations = new Map<string, JevEvaluation>();
|
||||
let failed = 0;
|
||||
let inputTokens = 0;
|
||||
let outputTokens = 0;
|
||||
let actualModel: string | undefined;
|
||||
let next = 0;
|
||||
|
||||
const worker = async (): Promise<void> => {
|
||||
while (next < candidates.length) {
|
||||
const candidate = candidates[next++];
|
||||
if (!candidate) continue;
|
||||
try {
|
||||
const response = JevResponseSchema.parse(
|
||||
await resolvedClient.systemOne({
|
||||
model: config.model,
|
||||
state: buildJevState(
|
||||
candidate.finding,
|
||||
candidate.sourceContext,
|
||||
config.maxContextChars,
|
||||
),
|
||||
questions: {
|
||||
support: choice(
|
||||
"Does this source context directly support or contradict this exact code-review finding? Select needs_reasoning whenever the answer depends on omitted context or non-local reasoning.",
|
||||
SUPPORT_CRITERIA,
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
inputTokens += response.usage.input_tokens;
|
||||
outputTokens += response.usage.output_tokens;
|
||||
actualModel = response.model;
|
||||
const fingerprint = fingerprintFinding(candidate.finding);
|
||||
evaluations.set(fingerprint, {
|
||||
fingerprint,
|
||||
model: response.model,
|
||||
support: response.answers.support.choice,
|
||||
confidence: response.answers.support.confidence,
|
||||
probabilities: response.answers.support.probabilities,
|
||||
});
|
||||
} catch {
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all(
|
||||
Array.from({ length: Math.min(CONCURRENCY, candidates.length) }, () => worker()),
|
||||
);
|
||||
|
||||
return {
|
||||
evaluations,
|
||||
summary: {
|
||||
configuredModel: config.model,
|
||||
...(actualModel ? { actualModel } : {}),
|
||||
durationMs: Date.now() - started,
|
||||
evaluated: evaluations.size,
|
||||
failed,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cost: (inputTokens * INPUT_COST_PER_MILLION) / 1_000_000,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** High-confidence local answers act; sensitive or uncertain answers defer. */
|
||||
export function jevDisposition(
|
||||
finding: Finding,
|
||||
evaluation: JevEvaluation | undefined,
|
||||
minConfidence: number,
|
||||
): "keep" | "drop" | "defer" {
|
||||
if (!evaluation || evaluation.confidence < minConfidence) return "defer";
|
||||
// Jev sees repository source, not the audited external passage. The reasoning
|
||||
// verifier remains responsible for citation support in either direction.
|
||||
if (finding.sources?.length) return "defer";
|
||||
if (evaluation.support === "supported") return "keep";
|
||||
if (evaluation.support === "needs_reasoning") return "defer";
|
||||
if (
|
||||
finding.severity === "critical" ||
|
||||
finding.category === "security" ||
|
||||
finding.category === "secrets"
|
||||
) {
|
||||
return "defer";
|
||||
}
|
||||
return "drop";
|
||||
}
|
||||
|
||||
/** Build the only state sent to TypeSafe: finding metadata plus bounded local source. */
|
||||
export function buildJevState(finding: Finding, sourceContext: string, maxContextChars: number) {
|
||||
return {
|
||||
finding: {
|
||||
severity: finding.severity,
|
||||
category: finding.category,
|
||||
file: boundText(finding.file, MAX_FILE_CHARS),
|
||||
line: finding.line ?? null,
|
||||
title: boundText(finding.title, MAX_TITLE_CHARS),
|
||||
rationale: boundText(finding.rationale, MAX_RATIONALE_CHARS),
|
||||
evidence: finding.evidence ? boundText(finding.evidence, MAX_EVIDENCE_CHARS) : null,
|
||||
},
|
||||
sourceContext: boundContext(sourceContext, finding.evidence, maxContextChars),
|
||||
};
|
||||
}
|
||||
|
||||
function boundText(value: string, maxChars: number): string {
|
||||
return value.length <= maxChars ? value : `${value.slice(0, maxChars - 1)}…`;
|
||||
}
|
||||
|
||||
function boundContext(context: string, evidence: string | undefined, maxChars: number): string {
|
||||
if (context.length <= maxChars) return context;
|
||||
const omitted = "\n... source omitted by ecr ...\n";
|
||||
const budget = Math.max(1, maxChars - omitted.length);
|
||||
const evidenceAt = evidence ? context.indexOf(evidence) : -1;
|
||||
if (evidenceAt >= 0) {
|
||||
const start = Math.max(0, evidenceAt - Math.floor(budget / 2));
|
||||
return context.slice(start, start + budget) + omitted;
|
||||
}
|
||||
const first = Math.floor(budget / 2);
|
||||
const last = budget - first;
|
||||
return context.slice(0, first) + omitted + context.slice(-last);
|
||||
}
|
||||
|
||||
function emptyResult(
|
||||
config: NonNullable<LoadedConfig["jev"]>,
|
||||
started: number,
|
||||
unavailable: string,
|
||||
): JevVerificationResult {
|
||||
return {
|
||||
evaluations: new Map(),
|
||||
summary: {
|
||||
configuredModel: config.model,
|
||||
durationMs: Date.now() - started,
|
||||
evaluated: 0,
|
||||
failed: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cost: 0,
|
||||
unavailable,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import type { CoordinatorOutput, Finding, ReviewMetadata, ReviewTrace } from "./
|
||||
import type { FilteredFile } from "./noise.js";
|
||||
import type { TokenUsage } from "./opencode.js";
|
||||
import type { ResearchProvenance } from "./research.js";
|
||||
import type { JevVerificationSummary } from "./jev.js";
|
||||
|
||||
export interface RunLogRecord {
|
||||
timestamp: string;
|
||||
@@ -21,6 +22,8 @@ export interface RunLogRecord {
|
||||
filteredFiles: FilteredFile[];
|
||||
/** Bounded queries/results plus grounded research decisions and final usefulness metrics. */
|
||||
research?: ResearchProvenance;
|
||||
/** Active Jev cascade usage and availability; individual drops use verifierDropped. */
|
||||
jevVerification?: JevVerificationSummary;
|
||||
agentCosts: Record<string, number>;
|
||||
totalCost: number;
|
||||
// Aggregate token usage across all agent + coordinator requests, for cache
|
||||
|
||||
+46
-8
@@ -20,10 +20,10 @@ import type { ResearchMcpRuntime } from "./research.js";
|
||||
export const CLAUDE_CODE_ENGINE = "claude-code" as const;
|
||||
|
||||
/** Fixed so repo config cannot redirect the Meta credential. */
|
||||
// @ref LLP 0003#muse-spark-via-meta-model-api [implements] — fixed endpoint and model allowlist
|
||||
// @ref LLP 0003#muse-spark-via-meta-model-api [implements] — fixed endpoint, provider routing, and known-model metadata
|
||||
export const META_MODEL_API_BASE_URL = "https://api.meta.ai/v1";
|
||||
|
||||
/** Supported public Muse models. Unknown ids remain visible to preflight. */
|
||||
/** Known public Muse models. Other explicit `meta/...` ids use neutral metadata. */
|
||||
export const META_MUSE_MODELS: Record<string, Record<string, unknown>> = {
|
||||
"muse-spark-1.2": {
|
||||
name: "Muse Spark 1.2",
|
||||
@@ -55,6 +55,36 @@ export const META_MUSE_MODELS: Record<string, Record<string, unknown>> = {
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
},
|
||||
"muse-spark-1.3": {
|
||||
name: "Muse Spark 1.3",
|
||||
reasoning: true,
|
||||
limit: { context: 1_048_576, output: 1_048_576 },
|
||||
modalities: {
|
||||
input: ["text", "image", "pdf", "video"],
|
||||
output: ["text"],
|
||||
},
|
||||
options: {
|
||||
store: false,
|
||||
reasoningEffort: "high",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
},
|
||||
"muse-spark-1.3-contributor": {
|
||||
name: "Muse Spark 1.3 Contributor",
|
||||
reasoning: true,
|
||||
limit: { context: 1_048_576, output: 1_048_576 },
|
||||
modalities: {
|
||||
input: ["text", "image", "pdf", "video"],
|
||||
output: ["text"],
|
||||
},
|
||||
options: {
|
||||
store: false,
|
||||
reasoningEffort: "high",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -265,7 +295,19 @@ export function buildOpencodeConfig(
|
||||
entry.tokenEnv &&
|
||||
!entry.upstream
|
||||
) {
|
||||
// Muse needs the Responses adapter; openai-compatible uses Chat Completions.
|
||||
// Meta's Model API uses the Responses adapter; openai-compatible uses Chat
|
||||
// Completions. Register every explicitly referenced `meta/…` model so a new
|
||||
// Meta family/name does not require an ECR release merely to select the right
|
||||
// provider. Known Muse ids get their published capabilities; unknown ids stay
|
||||
// neutral and are validated by Meta on their first request.
|
||||
const models: Record<string, unknown> = {};
|
||||
for (const id of referencedModels) {
|
||||
const slash = id.indexOf("/");
|
||||
if (slash > 0 && id.slice(0, slash) === "meta" && id.length > slash + 1) {
|
||||
const model = id.slice(slash + 1);
|
||||
models[model] = META_MUSE_MODELS[model] ?? {};
|
||||
}
|
||||
}
|
||||
provider.meta = {
|
||||
npm: "@ai-sdk/openai",
|
||||
name: "Meta Model API",
|
||||
@@ -273,11 +315,7 @@ export function buildOpencodeConfig(
|
||||
baseURL: META_MODEL_API_BASE_URL,
|
||||
apiKey: `{env:${entry.tokenEnv}}`,
|
||||
},
|
||||
models: Object.fromEntries(
|
||||
Object.entries(META_MUSE_MODELS).filter(([model]) =>
|
||||
referencedModels.includes(`meta/${model}`),
|
||||
),
|
||||
),
|
||||
models,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
+25
-1
@@ -84,6 +84,8 @@ import {
|
||||
summarizeResearchUsefulness,
|
||||
} from "./research.js";
|
||||
import type { ResearchEvidence, ResearchMcpRuntime, ResearchProvenance } from "./research.js";
|
||||
import { withoutJevCredential } from "./jev.js";
|
||||
import type { JevVerificationSummary } from "./jev.js";
|
||||
|
||||
export interface ReviewRunOptions {
|
||||
config: LoadedConfig;
|
||||
@@ -297,6 +299,7 @@ export async function runReview(
|
||||
|
||||
let researchEvidence: ResearchEvidence[] = [];
|
||||
let researchRecord: ResearchProvenance | undefined;
|
||||
let jevRecord: JevVerificationSummary | undefined;
|
||||
|
||||
// Materialize the PR-head tree (not the current checkout) when the source can, so
|
||||
// the agents' surrounding-source reads and the verifier's re-reads see the versions
|
||||
@@ -387,9 +390,16 @@ export async function runReview(
|
||||
// close it. Two separate try blocks keep the precise per-engine error messages.
|
||||
let opencodeHandle: OpencodeHandle | null = null;
|
||||
let claudeHandle: ClaudeCodeHandle | null = null;
|
||||
// OpenCode's SDK copies process.env into its server. Withhold the unrelated Jev
|
||||
// credential at spawn time, then restore the parent immediately; Claude uses an
|
||||
// allowlisted child env already. The captured value is passed directly to Jev.
|
||||
// @ref LLP 0014#active-selective-cascade [constrained-by] — reviewer engines never receive the Jev credential
|
||||
const jevApiKey = process.env.TYPESAFE_API_KEY;
|
||||
try {
|
||||
if (usesOpencode) {
|
||||
opencodeHandle = await startOpencode(buildOpencodeConfig(config, researchRuntime));
|
||||
opencodeHandle = await withoutJevCredential(() =>
|
||||
startOpencode(buildOpencodeConfig(config, researchRuntime)),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
await auth.cleanup();
|
||||
@@ -1089,6 +1099,9 @@ export async function runReview(
|
||||
process.cwd(),
|
||||
progress,
|
||||
researchEvidence,
|
||||
config.jev
|
||||
? { config: config.jev, ...(jevApiKey ? { apiKey: jevApiKey } : {}) }
|
||||
: undefined,
|
||||
);
|
||||
agentCosts["verifier"] = verification.cost;
|
||||
trackTokens("verifier", verification.tokens);
|
||||
@@ -1098,6 +1111,15 @@ export async function runReview(
|
||||
config.agents[0]?.model ?? config.coordinator.model,
|
||||
verification.model,
|
||||
);
|
||||
if (verification.jev) {
|
||||
jevRecord = verification.jev;
|
||||
agentCosts["jev-verifier"] = verification.jev.cost;
|
||||
trackTokens("jev-verifier", {
|
||||
input: verification.jev.inputTokens,
|
||||
output: verification.jev.outputTokens,
|
||||
});
|
||||
trackModel("jev-verifier", verification.jev.configuredModel, verification.jev.actualModel);
|
||||
}
|
||||
verifierDropped = verification.dropped;
|
||||
citationStrips = verification.citationStripped;
|
||||
if (verification.dropped.length > 0 || verification.citationStripped.length > 0) {
|
||||
@@ -1370,6 +1392,7 @@ export async function runReview(
|
||||
await safeLog(logPath, {
|
||||
...baseRecord,
|
||||
...(researchRecord ? { research: researchRecord } : {}),
|
||||
...(jevRecord ? { jevVerification: jevRecord } : {}),
|
||||
agentCosts,
|
||||
totalCost: sum(agentCosts),
|
||||
tokens: tokenTotals,
|
||||
@@ -1408,6 +1431,7 @@ export async function runReview(
|
||||
await safeLog(logPath, {
|
||||
...baseRecord,
|
||||
...(researchRecord ? { research: researchRecord } : {}),
|
||||
...(jevRecord ? { jevVerification: jevRecord } : {}),
|
||||
agentCosts,
|
||||
totalCost: sum(agentCosts),
|
||||
tokens: tokenTotals,
|
||||
|
||||
+75
-10
@@ -2,11 +2,15 @@
|
||||
// @ref LLP 0005#verifier-confinement-and-fail-open
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { TypeSafeClient } from "@typesafe-ai/sdk";
|
||||
|
||||
import { pathInside } from "./exec.js";
|
||||
import type { LoadedConfig } from "../config/schema.js";
|
||||
import { evaluateFindingsWithJev, jevDisposition } from "./jev.js";
|
||||
import type { JevVerificationSummary } from "./jev.js";
|
||||
import type { ResearchEvidence } from "./research.js";
|
||||
import type { Finding } from "./schema.js";
|
||||
import { parseVerdict } from "./schema.js";
|
||||
import { fingerprintFinding, parseVerdict } from "./schema.js";
|
||||
import { addTokenUsage, promptAndParse, VERIFIER_AGENT } from "./opencode.js";
|
||||
import type { OpencodeHandle, TokenUsage } from "./opencode.js";
|
||||
import { buildVerifierSystem, buildVerifierTask } from "./prompts.js";
|
||||
@@ -34,6 +38,8 @@ export interface VerificationResult {
|
||||
tokens: TokenUsage;
|
||||
/** provider/model that actually answered the verify calls (see PromptResult.model). */
|
||||
model?: string;
|
||||
/** Active selective-verification usage; absent when Jev is not configured. */
|
||||
jev?: JevVerificationSummary;
|
||||
}
|
||||
|
||||
/** The audited passages behind a finding's grounded citations, bounded per source. */
|
||||
@@ -94,10 +100,13 @@ export function matchEvidence(evidence: string, content: string): "present" | "a
|
||||
|
||||
// @ref LLP 0005#verifier-confinement-and-fail-open [implements] — pathInside gate: out-of-tree reads (and their present/absent verdict) refused
|
||||
/** Read the cited file and grade the evidence against it (see matchEvidence). */
|
||||
async function evidencePresence(
|
||||
async function inspectEvidence(
|
||||
finding: Finding,
|
||||
cwd: string,
|
||||
): Promise<"present" | "absent" | "unknown"> {
|
||||
): Promise<{
|
||||
presence: "present" | "absent" | "unknown";
|
||||
sourceContext: string;
|
||||
}> {
|
||||
// finding.file is an unconstrained, LLM-authored string produced over untrusted PR
|
||||
// content, so a prompt-injected finding could point it at a host secret. path.resolve
|
||||
// IGNORES cwd when finding.file is already absolute (e.g. ~/.claude/.credentials.json),
|
||||
@@ -107,15 +116,15 @@ async function evidencePresence(
|
||||
// materialized PR-head tree); anything outside is uncheckable, never read.
|
||||
const resolved = path.resolve(cwd, finding.file);
|
||||
if (!pathInside(resolved, cwd)) {
|
||||
return "unknown";
|
||||
return { presence: "unknown", sourceContext: "" };
|
||||
}
|
||||
let content: string;
|
||||
try {
|
||||
content = await readFile(resolved, "utf8");
|
||||
} catch {
|
||||
return "unknown";
|
||||
return { presence: "unknown", sourceContext: "" };
|
||||
}
|
||||
return matchEvidence(finding.evidence ?? "", content);
|
||||
return { presence: matchEvidence(finding.evidence ?? "", content), sourceContext: content };
|
||||
}
|
||||
|
||||
// @ref LLP 0005#verifier-confinement-and-fail-open [constrained-by] — fails open: a verify error/timeout keeps the finding, never drops it
|
||||
@@ -145,6 +154,12 @@ export async function verifyFindings(
|
||||
onProgress?: (message: string) => void,
|
||||
/** This run's audited research evidence, for findings that cite documentation. */
|
||||
researchEvidence: ResearchEvidence[] = [],
|
||||
jev?: {
|
||||
config: NonNullable<LoadedConfig["jev"]>;
|
||||
apiKey?: string;
|
||||
/** Test seam; production constructs the pinned client inside jev.ts. */
|
||||
client?: TypeSafeClient;
|
||||
},
|
||||
): Promise<VerificationResult> {
|
||||
const dropped: Array<{ finding: Finding; reason: string }> = [];
|
||||
const citationStripped: Array<{ finding: Finding; reason: string }> = [];
|
||||
@@ -156,9 +171,29 @@ export async function verifyFindings(
|
||||
|
||||
// Phase 1 — deterministic quote-grounding for every finding.
|
||||
const checked = await Promise.all(
|
||||
findings.map(async (finding) => ({ finding, presence: await evidencePresence(finding, cwd) })),
|
||||
findings.map(async (finding) => ({ finding, ...(await inspectEvidence(finding, cwd)) })),
|
||||
);
|
||||
|
||||
// Phase 2 — active selective classification. A confident local answer can
|
||||
// avoid the slower verifier; uncertainty is explicitly handed to it.
|
||||
const jevResult = jev
|
||||
? await evaluateFindingsWithJev({
|
||||
config: jev.config,
|
||||
candidates: checked.map(({ finding, sourceContext }) => ({ finding, sourceContext })),
|
||||
...(jev.apiKey ? { apiKey: jev.apiKey } : {}),
|
||||
...(jev.client ? { client: jev.client } : {}),
|
||||
})
|
||||
: undefined;
|
||||
if (jevResult?.summary.unavailable) {
|
||||
onProgress?.(
|
||||
` Jev unavailable (${jevResult.summary.unavailable}); using normal verification.`,
|
||||
);
|
||||
} else if (jevResult) {
|
||||
onProgress?.(
|
||||
` Jev classified ${jevResult.summary.evaluated} finding(s); ${jevResult.summary.failed} fell back.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Decide which findings need an LLM check vs. can be kept directly. A finding
|
||||
// that cites documentation always gets an LLM check: the repo alone cannot
|
||||
// confirm an external-behavior claim, and the verifier must judge whether the
|
||||
@@ -171,14 +206,36 @@ export async function verifyFindings(
|
||||
}> = [];
|
||||
for (const { finding, presence } of checked) {
|
||||
const citedSources = citedSourcesFor(finding, researchEvidence);
|
||||
if (presence === "absent" || finding.severity === "critical" || citedSources) {
|
||||
const evaluation = jevResult?.evaluations.get(fingerprintFinding(finding));
|
||||
const disposition = evaluation
|
||||
? jevDisposition(finding, evaluation, jev?.config.minConfidence ?? 1)
|
||||
: undefined;
|
||||
if (disposition === "keep") {
|
||||
verdicts.set(finding, "keep");
|
||||
continue;
|
||||
}
|
||||
if (disposition === "drop") {
|
||||
verdicts.set(finding, "drop");
|
||||
dropped.push({
|
||||
finding,
|
||||
reason: `Jev directly contradicted the finding (${Math.round(evaluation!.confidence * 100)}% confidence)`,
|
||||
});
|
||||
onProgress?.(` Jev: dropped ${finding.severity} "${finding.title}" as contradicted.`);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
disposition === "defer" ||
|
||||
presence === "absent" ||
|
||||
finding.severity === "critical" ||
|
||||
citedSources
|
||||
) {
|
||||
toVerify.push({ finding, presence, ...(citedSources ? { citedSources } : {}) });
|
||||
} else {
|
||||
verdicts.set(finding, "keep"); // grounded (or uncheckable) non-critical
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2 — LLM verify (parallel). Refuted → drop; verified or errored → keep.
|
||||
// Phase 3 — reasoning verify (parallel). Refuted → drop; verified or errored → keep.
|
||||
await Promise.all(
|
||||
toVerify.map(async ({ finding, presence, citedSources }, index) => {
|
||||
try {
|
||||
@@ -242,5 +299,13 @@ export async function verifyFindings(
|
||||
const kept = findings
|
||||
.filter((finding) => verdicts.get(finding) === "keep")
|
||||
.map((finding) => replacements.get(finding) ?? finding);
|
||||
return { kept, dropped, citationStripped, cost, tokens, model };
|
||||
return {
|
||||
kept,
|
||||
dropped,
|
||||
citationStripped,
|
||||
cost,
|
||||
tokens,
|
||||
model,
|
||||
...(jevResult ? { jev: jevResult.summary } : {}),
|
||||
};
|
||||
}
|
||||
@@ -114,6 +114,8 @@ jobs:
|
||||
# META_API_KEY: ${{ secrets.META_API_KEY }}
|
||||
# Optional search-only credential for trusted platform documentation research.
|
||||
BRAVE_SEARCH_API_KEY: ${{ secrets.BRAVE_SEARCH_API_KEY }}
|
||||
# Optional Jev verification credential (used when the root config has `jev`).
|
||||
TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }}
|
||||
# Optional: override the model for every agent.
|
||||
REVIEWER_MODEL: ${{ vars.REVIEWER_MODEL }}
|
||||
run: |
|
||||
|
||||
@@ -152,6 +152,8 @@ jobs:
|
||||
# META_API_KEY: ${{ secrets.META_API_KEY }}
|
||||
# Optional search-only credential for trusted platform documentation research.
|
||||
BRAVE_SEARCH_API_KEY: ${{ secrets.BRAVE_SEARCH_API_KEY }}
|
||||
# Optional Jev verification credential (used when the root config has `jev`).
|
||||
TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }}
|
||||
# Optional: override the model for every agent.
|
||||
REVIEWER_MODEL: ${{ vars.REVIEWER_MODEL }}
|
||||
AGENTS: ${{ steps.cmd.outputs.agents }}
|
||||
|
||||
+13
-1
@@ -48,6 +48,18 @@
|
||||
// "timeoutMs": 30000
|
||||
// },
|
||||
|
||||
// Optional Jev verification cascade (ROOT-ONLY). Adding this block activates it:
|
||||
// confident local support keeps a finding, confident contradiction drops an
|
||||
// ordinary finding, and uncertainty or protected findings defer to the reasoning
|
||||
// verifier. Pin the model because moving aliases can invalidate the threshold.
|
||||
// Set TYPESAFE_API_KEY in the environment before adding the block.
|
||||
// "jev": {
|
||||
// "model": "jev-1.13.0",
|
||||
// "minConfidence": 0.9,
|
||||
// "timeoutMs": 10000,
|
||||
// "maxContextChars": 30000
|
||||
// },
|
||||
|
||||
// Large diffs are split into focused chunks by changed-line count, plus a
|
||||
// cross-cutting pass for multi-file issues. Diffs under maxChangedLines are one
|
||||
// full-context pass. Defaults shown; raise/lower per your model + PR sizes.
|
||||
@@ -87,7 +99,7 @@
|
||||
// "auth": { "mode": "api-key", "provider": "openai", "tokenEnv": "OPENAI_API_KEY" }
|
||||
//
|
||||
// Muse Spark via the public Meta Model API Responses endpoint:
|
||||
// set models to `meta/muse-spark-1.2` and configure:
|
||||
// set models to `meta/muse-spark-1.3` and configure:
|
||||
// "auth": { "providers": {
|
||||
// "meta": { "mode": "api-key", "tokenEnv": "META_API_KEY" }
|
||||
// } }
|
||||
|
||||
@@ -118,6 +118,8 @@ jobs:
|
||||
# META_API_KEY: ${{ secrets.META_API_KEY }}
|
||||
# Optional search-only credential for trusted platform documentation research.
|
||||
BRAVE_SEARCH_API_KEY: ${{ secrets.BRAVE_SEARCH_API_KEY }}
|
||||
# Optional Jev verification credential (used when the root config has `jev`).
|
||||
TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }}
|
||||
# Optional: override the model for every agent.
|
||||
REVIEWER_MODEL: ${{ vars.REVIEWER_MODEL }}
|
||||
|
||||
|
||||
Reference in new issue
Block a user