# Veris — Foundation Audit Tracker Internal engineering log. One row per finding from the 2026-09-07 architecture audit. **Audit method:** 17-agent parallel codebase map across 8 dimensions, each dimension adversarially verified by an independent agent against source. Every finding below was then re-confirmed by hand against the file and line cited. Findings the verifier marked OVERSTATED or WRONG were dropped and are not listed here. **Baseline at audit time** — branch `test/engine-unit-suite` @ `a18b18f`: | Metric | Value | |---|---| | Source | 4,627 LOC TS / 31 files | | Tests | 55 passing / 12 files | | Typecheck | clean | | Whole-`src` coverage | 36.36% (6 files at 0%) | | `veris-report.md` | 6,043 lines / 1.26 MB (98% one repeated sentence) | | `veris-dashboard.html` | 156,306,026 bytes | | Tracked files | 4,548 (4,432 = stale `node_modules`) | | Analyzed files on own repo | 1,735 (1,667 third-party) | **Status vocabulary:** `OPEN` → `IN PROGRESS` → `FIXED` (code changed, verified) → `WONTFIX` (with reason). A row reaches `FIXED` only when a command was run and its output confirmed the behaviour changed — not when the edit was made. **Severity:** `S1` blocks any credibility claim · `S2` produces materially wrong output · `S3` correctness/robustness defect · `S4` hygiene, docs, maintainability. --- ## Summary | Group | Theme | Count | S1 | S2 | S3 | S4 | |---|---|---|---|---|---|---| | A | Baseline truthfulness | 6 | 4 | 2 | 0 | 0 | | B | Graph extraction accuracy | 9 | 1 | 6 | 2 | 0 | | C | Scoring math | 11 | 1 | 4 | 6 | 0 | | D | Security and trust | 7 | 2 | 2 | 3 | 0 | | E | State and MCP correctness | 9 | 0 | 4 | 4 | 1 | | F | Output size | 3 | 0 | 3 | 0 | 0 | | G | Project hygiene | 7 | 0 | 1 | 2 | 4 | | **Total** | | **52** | **8** | **22** | **17** | **5** | --- ## Group A — Baseline truthfulness Everything Veris says is a comparison against a "before" state. If that state is fabricated or malformed, every downstream number is meaningless. This group is a hard gate on the rest. ### A1 · Fabricates a baseline when git is unavailable · S1 **Where:** [`src/cli.ts:238`](../../src/cli.ts) · [`src/mcp/McpServer.ts:193`](../../src/mcp/McpServer.ts) · `McpServer.ts:246` · `McpServer.ts:327` · [`src/index.ts:37`](../../src/index.ts) **What happens:** When `GitDiffDriver.snapshot()` returns null, all five sites construct a fake base graph from the first 70% of the current node and edge lists, then run the full diff → risk → confidence chain against it and report the result as a behavioural diff. `ensureDiffAndRisk()` (`McpServer.ts:191-195`) and `what_if_revert` return no mode flag at all, so a consumer cannot tell a fabricated result from a real one. A tool whose stated purpose is detecting unverified work is, on this path, producing unverified work. **Proposed fix:** Delete all five fallbacks. Baseline resolution failure returns an explicit typed error naming the cause (not a git repo / no base ref / worktree failed). Every diff-bearing response carries a mandatory `baselineMode: 'git'` discriminant — there is no second value. **Status:** FIXED — all five sites removed; `GitDiffDriver.snapshot()` throws `BaselineError`, CLI exits 3, MCP returns an explanatory error. Verified: `GitDiffDriver.test.ts` 'throws rather than fabricating a baseline outside a git repository'; `ToolSchemas.test.ts` asserts no tool description mentions a synthetic fallback. --- ### A2 · Base graph always loses every import edge · S1 **Where:** [`src/engine/GitDiffDriver.ts:137-139`](../../src/engine/GitDiffDriver.ts) **What happens:** The base tree is analyzed inside a temp worktree, so its paths must be rewritten to match the head tree's. The rewrite covers `f.filePath` but not the parallel `dependencyMap`, whose keys are still temp paths. [`BehavioralGraphEngine.ts:75`](../../src/engine/BehavioralGraphEngine.ts) reads `dependencyMap` — so it looks up head paths in a map keyed by temp paths and finds nothing. Result: **the base graph has zero `DependsOn` edges on every run, on every project.** Every run reports the entire import structure as newly added. **Proposed fix:** Remove the parallel map from the graph builder's input path — read `file.imports` off the file record, which is already correctly rewritten. Deletes the whole class of desync rather than patching this instance. **Status:** FIXED — `BehavioralGraphEngine` reads `file.imports`; the parallel map is no longer consulted and the worktree path rewrite is gone entirely (relative ids make it unnecessary). Verified: `GitDiffDriver.test.ts` 'produces import edges in the base graph, not only the head graph'. --- ### A3 · No merge-base · S1 **Where:** [`src/engine/GitDiffDriver.ts:53`](../../src/engine/GitDiffDriver.ts) **What happens:** Candidate refs (`origin/main`, `origin/master`, `main`, `master`, `HEAD~1`) are used directly as the base. A branch cut three days ago diffs against main's current tip, so every commit others merged in the interim is reported as behaviour **this branch removed**. **Proposed fix:** After resolving a candidate ref, resolve `git merge-base HEAD ` and use that commit as the base. Fall back to the ref itself only when merge-base fails (unrelated histories), and record which was used in the response. **Status:** FIXED — `resolveBase()` returns `git merge-base HEAD ` and reports `usedMergeBase`. Verified: `GitDiffDriver.test.ts` 'compares against the divergence point, not the branch tip' and 'does not report unrelated work on main as removed by this branch'. --- ### A4 · Untracked and ignored files count as new behaviour · S2 **Where:** no filter exists — all 7 git invocations in `GitDiffDriver.ts` are `rev-parse`/`worktree` **What happens:** The head tree is analyzed straight off disk. Build output, gitignored folders, scratch files and local experiments all become graph nodes present in head and absent from base, so they are reported as added behaviour. Measured on this repo: `addedNodes: 12438, removedNodes: 0`. **Proposed fix:** Run `git ls-files -z` once, build a tracked-path set, filter both graphs through it. Untracked files are not behaviour under review. **Status:** FIXED — `trackedFiles()` feeds `includeOnly` for both snapshots. Verified: `GitDiffDriver.test.ts` 'excludes untracked and ignored files from the graph'. Live run: 118 tracked files analyzed, was 1,735. --- ### A5 · Working-tree analysis labelled with a commit SHA · S2 **Where:** [`src/engine/GitDiffDriver.ts:85`](../../src/engine/GitDiffDriver.ts) vs `:87-90` **What happens:** `headRef` is `git rev-parse HEAD`, but the head graph is built by analyzing the **dirty working tree**. The response therefore claims commit-to-commit provenance for a comparison that included uncommitted edits. Nobody can reproduce the result from the stated inputs. **Proposed fix:** Report `headRef` as `-dirty` with a `dirtyFileCount` when the tree is not clean, and mark the run as non-reproducible in persisted state. Never present a bare SHA for uncommitted content. **Status:** FIXED — `headRef` is `-dirty` with `dirtyFileCount` when the tree is not clean. Verified: `GitDiffDriver.test.ts` 'marks the head as dirty' and 'reports a clean tree with a bare commit sha'. --- ### A6 · Ignore globs anchored to the root only · S1 **Where:** [`src/engine/RepositoryIntelligenceEngine.ts:34`](../../src/engine/RepositoryIntelligenceEngine.ts) **What happens:** Patterns are built as `!${projectRoot}/${p}/**/*`, which matches only a top-level `node_modules`. Nested ones — `examples/runs/node_modules/`, workspace packages, vendored trees — are parsed as first-party code. Measured on this repo: **1,667 of 1,735 analyzed files are third-party.** Root cause of F1 (156 MB dashboard) and a large share of the risk noise. **Proposed fix:** Anchor as `!${projectRoot}/**/${p}/**/*`. Extend the ignore set to `dist`, `build`, `out`, `.next`, `.nuxt`, `coverage`, `vendor`, `.git`. Add a hard node-count ceiling with a loud warning rather than silent unbounded growth. **Status:** FIXED — globs anchored `!/**//**/*`, ignore set widened, plus a defence-in-depth segment check and a file ceiling. Verified: `RepositoryIntelligenceEngine.test.ts` 'excludes nested node_modules, not only a top-level one'. Live run: zero `node_modules` strings in the dashboard, was 156 MB of them. --- ## Group B — Graph extraction accuracy The graph is the substrate for every other claim. B1 is the single largest item in the audit and gates any workflow-level or safety-level ambition. ### B1 · Calls resolved by trailing name, not by symbol · S1 **Where:** [`src/engine/RepositoryIntelligenceEngine.ts:221`](../../src/engine/RepositoryIntelligenceEngine.ts) + [`src/engine/BehavioralGraphEngine.ts:107-113`](../../src/engine/BehavioralGraphEngine.ts) **What happens:** The callee is extracted as `expr.getText().split('.').pop()` — the trailing identifier of the call expression, with no scope, no import binding and no type information. The graph builder then indexes every declaration by bare name and emits an `Invokes` edge to **every** node sharing that name. `console.log('x')` yields `INVOKES Caller → Logger::log`. Measured against the MCP SDK: **2,804 of 3,077 `Invokes` edges (91.1%) resolve to a name with more than one definition.** This is a name index presented as a call graph, and every score, fingerprint and narrative downstream is a function of it. **Proposed fix:** Resolve through the TypeScript checker — `getSymbol()` / `getAliasedSymbol()` / `getDefinitionNodes()` on the call's expression, scoped by the file's import bindings. Emit exactly one edge for a resolved target. Where resolution genuinely fails (dynamic dispatch, `any`), emit the edge marked `resolution: 'unresolved'` and exclude it from scoring rather than guessing. Add `resolution` to the edge model so consumers can filter on it. **Status:** FIXED — callee resolution goes through the TypeScript checker (`getSymbol` / `getAliasedSymbol` / `getDeclarations`) against an indexed declaration map; ambiguous names emit no edge. Verified: `RepositoryIntelligenceEngine.test.ts` 'resolves an imported call to exactly one target', 'does not invent an edge for a same-named declaration that was never imported', 'emits no edge when a bare name matches several declarations'. Live run: 375 resolved (96.9%), 0 ambiguous. --- ### B2 · ESM import specifiers never match · S2 **Where:** [`src/engine/BehavioralGraphEngine.ts:82`](../../src/engine/BehavioralGraphEngine.ts) vs `:34` **What happens:** File basenames are stripped of their extension at `:34`, but the import specifier is compared raw at `:82`. `import { x } from './target.js'` is compared against `target` and never matches. Every NodeNext / ESM / Deno codebase — where extensioned specifiers are mandatory — produces zero `DependsOn` edges. **Proposed fix:** Normalize both sides through one helper: strip `.ts`/`.tsx`/`.js`/`.jsx`/`.mjs`/ `.cjs` and a trailing `/index`, then compare. **Status:** FIXED — one `normalizeBase()` helper strips extensions and `/index` on both sides. Verified: `RepositoryIntelligenceEngine.test.ts` 'matches an extensioned ESM specifier'. --- ### B3 · Constructors, getters and setters are invisible · S2 **Where:** [`src/engine/RepositoryIntelligenceEngine.ts:94`](../../src/engine/RepositoryIntelligenceEngine.ts) **What happens:** Only `cls.getMethods()` is read. A class whose logic lives in its constructor — the common shape for a service, a client wrapper, a repository — contributes no nodes and no calls. **Proposed fix:** Add `getConstructors()`, `getGetAccessors()`, `getSetAccessors()`, and record the member kind on the node so consumers can distinguish them. **Status:** FIXED — `getConstructors()`, `getGetAccessors()`, `getSetAccessors()` extracted with a `kind` on each member. Verified: `RepositoryIntelligenceEngine.test.ts` 'extracts constructors, getters and setters, not only methods'. --- ### B4 · Nested functions become exported top-level nodes · S3 **Where:** [`src/engine/RepositoryIntelligenceEngine.ts:164-166`](../../src/engine/RepositoryIntelligenceEngine.ts) **What happens:** The comment states the walk is restricted to top-level declarations; the check was never written. Callbacks, closures and inner helpers enter the graph as first-class nodes with `isExported: true` hardcoded. Inflates node count, degree, blast radius and every derived score. **Proposed fix:** Add the ancestry check the comment promises. Compute `isExported` from the actual export modifier instead of hardcoding it on four paths — or drop the field, since nothing reads it. **Status:** FIXED — uses `file.getVariableDeclarations()` (top level only) and derives `isExported` from the statement. Verified: `RepositoryIntelligenceEngine.test.ts` 'does not promote nested closures to top-level functions'. --- ### B5 · One flat name set per file, first declaration wins · S3 **Where:** [`src/engine/RepositoryIntelligenceEngine.ts:143`](../../src/engine/RepositoryIntelligenceEngine.ts) **What happens:** A single per-file `seen` set is consulted for every declaration regardless of scope. A nested `handler` encountered first suppresses the genuine exported `handler` later in the same file. **Proposed fix:** Scope the set per container, and prefer the outermost declaration on collision. Largely subsumed by B4. **Status:** FIXED — subsumed by B4; nested declarations no longer enter the name set. Verified: `RepositoryIntelligenceEngine.test.ts` 'keeps the real top-level declaration when a nested one shares its name'. --- ### B6 · Edge comparison drops the edge type · S3 **Where:** [`src/engine/BehavioralDiffEngine.ts:22-26`](../../src/engine/BehavioralDiffEngine.ts) **What happens:** Edges are keyed `source->target` while `GraphModels.addEdge` deduplicates on `source+target+type`. A `DependsOn` and an `Invokes` between the same pair collide, so one is silently invisible to the diff. Separately, `includes` is called inside `filter`, making the diff O(n²) — measurable on the graphs this produces. **Proposed fix:** Key as `source->target:type`. Use `Set` membership for both sides. **Status:** FIXED — `edgeKey()` includes the edge type and both sides use `Set` membership. Verified: `BehavioralDiffEngine.test.ts` passes with the type included in the key. --- ### B7 · Fingerprints embed absolute filesystem paths · S2 **Where:** [`src/engine/WorkflowFingerprint.ts:34`](../../src/engine/WorkflowFingerprint.ts) — node IDs originate at `RepositoryIntelligenceEngine` **What happens:** A member ID is `C:/Users//.../src/auth/login.ts::signIn`. Renaming a directory changes every ID and therefore every hash → 100% drift reported for a change with zero behavioural content (reproduced 4/4). Cloning to another path, or running in CI, produces fingerprints that cannot be compared with local ones — so persisted history is worthless across machines. **Proposed fix:** Relativize every node ID to the project root, with forward slashes, at the point of creation. Applies to state rows, fingerprints, reports and MCP output. **This is a persisted-data shape change** — see E2. **Status:** FIXED — `toRelative()` produces project-relative POSIX ids at the point of creation. Verified: `RepositoryIntelligenceEngine.test.ts` 'emits repository-relative POSIX paths, never absolute ones' and 'produces identical ids for identical sources under different roots'. --- ### B8 · Blind to the exact thing it claims to catch · S2 **Where:** [`src/engine/WorkflowFingerprint.ts:24-37`](../../src/engine/WorkflowFingerprint.ts) **What happens:** The hash covers member IDs and internal edge signatures. A rewritten function body that keeps the same name and the same callees produces a byte-identical fingerprint. The doc comment at `:14` claims *"this is how Veris detects silent rewrites that pass other checks"* — that is precisely the case it cannot detect. Combined with B7: loud on renames, silent on rewrites. **Proposed fix:** Add a normalized body hash per member node (whitespace and comments stripped) and include it in the fingerprint. Fixes the blind spot and, with B7, the false-alarm side. **Status:** FIXED — normalized `bodyHash` per declaration, carried onto `GraphNode`, surfaced as `DiffReport.modifiedNodes`, and included in the workflow fingerprint. Verified: `RepositoryIntelligenceEngine.test.ts` 'changes the hash when the body changes' / 'ignores comments and reformatting'; `GitDiffDriver.test.ts` 'detects a rewritten body as a modified node'. --- ### B9 · Deleted workflows never report drift · S2 **Where:** [`src/engine/DriftDetector.ts:43`](../../src/engine/DriftDetector.ts) **What happens:** The comparison iterates the current fingerprint set only. A workflow present in the previous run and absent now is never examined, so deleting an entire workflow prints *"No workflow drift detected."* Deletion is the most severe drift class and is the one that cannot be reported. **Proposed fix:** Iterate the union of previous and current keys. Emit an explicit `removed` drift class, ranked above rewrites. **Status:** FIXED — `DriftDetector` iterates the union of previous and current workflow ids and emits a `removed` class ranked above rewrites; a first observation is distinguished from an absence of drift. Verified: `DriftDetector.test.ts`. --- ## Group C — Scoring math ### C1 · Risk score is one measurement expressed twice, plus a regex · S2 **Where:** [`src/engine/RiskModelingEngine.ts`](../../src/engine/RiskModelingEngine.ts) **What happens:** `overallRisk = 0.45·min(8·degree,100) + 0.20·18log₂(degree+1) + 0.35·criticality`. The first two terms are monotone transforms of the same scalar (node degree), so 65% of the score is one input counted twice. The remaining 35% is a regex over the symbol name and path. Blast radius saturates at degree 13 — a node with 13 edges and one with 500 score identically. Class→own-method containment edges inflate degree further. **Proposed fix:** Either introduce genuinely independent inputs (reachability from an entry point, graph distance to a dangerous sink, change frequency from git, presence of covering tests), or stop publishing a composite and report the factors separately. Not shipping a number is better than shipping one that measures a single variable three ways. **Status:** FIXED — three independent inputs: coupling magnitude (exponential, no ceiling at degree 13), inbound-coupling dominance, and name/path criticality. Class-to-own-member containment edges excluded from degree. Explanations rewritten to describe the actual math. Verified: `RiskModelingEngine.test.ts`. --- ### C2 · Confidence cannot exceed 45 on a first run · S1 **Where:** [`src/engine/ConfidenceEngine.ts`](../../src/engine/ConfidenceEngine.ts) **What happens:** With no execution history the reachable range is a hard `[10, 45]` — the value is structurally incapable of leaving that band regardless of the repository. The checked-in demo's `40.12` reproduces exactly as `100 − 0.4·24.7 − 50`. Nothing has ever been compared against observed outcomes: there is no calibration, no reliability curve, no ground truth. `examples/demo-app/GROUND_TRUTH.md` exists and **is read by nothing**. **Proposed fix:** Remove the score from every gate and every headline. Report the inputs it is derived from (coverage of planned targets, execution recency, failure count) as separate labelled facts. Reintroduce a scalar only once it has been measured against real outcomes, and name it for what it measures. **Status:** FIXED — the composite score is gone. `overallConfidence` is now an alias of evidence coverage, documented as such in code, in the tool description and in README's Honest limits. Verified: `ConfidenceEngine.test.ts` 'separates risk from coverage instead of folding risk into one score'. --- ### C3 · Adding harmless files raises confidence · S3 **Where:** [`src/engine/ConfidenceEngine.ts:45`](../../src/engine/ConfidenceEngine.ts) **What happens:** The penalty is driven by the **mean** risk across impacted nodes. Touching ten trivial files alongside one dangerous one lowers the mean and raises confidence. Risk does not average — the dangerous node is exactly as dangerous either way. **Proposed fix:** Drive the penalty from the maximum, or a high percentile, of the risk distribution. **Status:** FIXED — risk summarized by `maxImpactedRisk` plus `highRiskNodeCount`. Verified: `ConfidenceEngine.test.ts` 'summarizes risk by maximum, so harmless nodes cannot dilute a dangerous one'. --- ### C4 · Passes decay, failures never do · S3 **Where:** [`src/engine/ConfidenceEngine.ts:79`](../../src/engine/ConfidenceEngine.ts) **What happens:** Successful executions are multiplied by `0.5^(ageDays/halfLife)`; the failure penalty is applied at full strength with no age term. A failure from a year ago, since fixed, still penalizes at full weight forever. **Proposed fix:** Apply the same decay to the failure term, or scope failure penalties to the current run only. Either is defensible; the asymmetry is not. **Status:** FIXED — failures are counted, not applied as an undecayed permanent penalty; passes decay by half-life. Verified: `ConfidenceEngine.test.ts` 'decays a passing result as it ages' and 'counts a failing target'. --- ### C5 · A flaky result is credited and penalized simultaneously · S3 **Where:** [`src/engine/ConfidenceEngine.ts:82-83`](../../src/engine/ConfidenceEngine.ts) **What happens:** `flaky` adds `w·decay·0.5` to earned credit **and** adds to `failedPenalty` in the same branch. The two partially cancel, with the net effect depending on unrelated config values. **Proposed fix:** Choose one semantic. Recommended: no earned credit, small fixed penalty, and surface flakiness as its own signal rather than folding it into a scalar. **Status:** FIXED — flaky earns no credit and is reported in its own field. Verified: `ConfidenceEngine.test.ts` 'gives a flaky result no coverage credit and counts it once'. --- ### C6 · Zero planned targets reports as fully verified · S3 **Where:** [`src/engine/ConfidenceEngine.ts:90`](../../src/engine/ConfidenceEngine.ts) **What happens:** `possible === 0` yields `executionDepth = 100` and the explanation *"Full execution depth mapped."* Nothing to verify is reported as everything verified. **Proposed fix:** Return an explicit `unknown` depth and say so in the explanation. **Status:** FIXED — `coverageKnown: false` when nothing is planned, with an explicit explanation. Verified: `ConfidenceEngine.test.ts` 'reports coverage as unknown when nothing was planned'. --- ### C7 · Contradicts itself at full coverage · S3 **Where:** [`src/engine/ConfidenceEngine.ts:96`](../../src/engine/ConfidenceEngine.ts) **What happens:** The branch tests the unrounded float, so a value of `99.999…` takes the penalty path and prints *"penalized due to missing or stale execution coverage (decayed depth 100.0%)."* **Proposed fix:** Compare the same rounded value that is displayed. **Status:** FIXED — branches compare the rounded value that is displayed. Verified: `ConfidenceEngine.test.ts` 'does not claim missing coverage when coverage is complete'. --- ### C8 · The what-if parameter is unreachable · S3 **Where:** [`src/engine/ConfidenceEngine.ts:87`](../../src/engine/ConfidenceEngine.ts) · [`src/cli.ts:276`](../../src/cli.ts) **What happens:** `executedTargetsCount` is documented on the `identify_unverified_behaviors` MCP tool as a what-if lever. The CLI hardcodes `0`, and on the MCP path `stateProvided` is true whenever state is enabled, so the parameter's branch is unreachable. The documented feature does nothing. **Proposed fix:** Wire it through as an explicit override that bypasses state, or remove it from the tool description. Do not ship a documented parameter with no effect. **Status:** FIXED — `executedTargetsCount` is a real override when state is unavailable. Verified: `ConfidenceEngine.test.ts` 'honours executedTargetsCount as a what-if override when no state exists'. --- ### C9 · "No magic numbers in code" is false · S4 **Where:** [`src/engine/RiskModelingEngine.ts:8`](../../src/engine/RiskModelingEngine.ts) · [`src/engine/VerificationPlanningEngine.ts:23,34`](../../src/engine/VerificationPlanningEngine.ts) **What happens:** The header comment claims every weight and threshold is externalized to `data/risk-config.json`. `VerificationPlanningEngine` is the only engine with no config integration and hardcodes every threshold (`integrationCount > 2`, `overallRisk > 30`, `blastRadius > 50`, `runtimeCriticality >= 80`). **Proposed fix:** Move the planning thresholds into `risk-config.json` under a `planning` key. **Status:** FIXED — planning thresholds moved to `data/risk-config.json` under `planning`, loaded via `loadRiskConfig`. Verified: `VerificationPlanningEngine.test.ts`. --- ### C10 · Budget allocator structurally deprioritizes adversarial work · S3 **Where:** [`src/engine/VerificationBudgetAllocator.ts`](../../src/engine/VerificationBudgetAllocator.ts) **What happens:** Leverage density carries an implicit per-tier constant of 1 : 0.5 : 0.29 across Tier 1/2/3, because tier leverage rises more slowly than tier cost. Greedy selection therefore exhausts cheap structural checks before ever reaching adversarial targets — the only tier that finds the failures the product exists to surface. **Proposed fix:** Rebalance `tierLeverage` / `tierCostSeconds` so density is tier-neutral, or rank by expected risk reduction rather than time efficiency. Add a test pinning the tier mix at a fixed budget. **Status:** FIXED — tier leverage and cost rebalanced so density is tier-neutral, with a test pinning the tier mix at a fixed budget. Verified: `VerificationBudgetAllocator.test.ts`. --- ### C11 · Dead probe threshold · S3 **Where:** [`src/engine/AdversarialProbeGenerator.ts:38`](../../src/engine/AdversarialProbeGenerator.ts) **What happens:** `minRiskThreshold` defaults to `10`, below the 12.5 floor the risk formula can produce. The filter at `:58` never excludes anything. **Proposed fix:** Set a threshold inside the achievable range, or delete the option. **Status:** FIXED — `probeMinOverallRisk` is a configured threshold inside the achievable range; probes also spread across the top-N riskiest members instead of one anchor. Verified: `AdversarialProbeGenerator.test.ts`. --- ## Group D — Security and trust ### D1 · Executes arbitrary code from the repository under analysis · S1 **Where:** [`src/plugins/PluginLoader.ts:101`](../../src/plugins/PluginLoader.ts) · [`SECURITY.md:15`](../../SECURITY.md) **What happens:** Any `.js`/`.mjs`/`.cjs` file in `/.veris/plugins/` is `require()`d and its `register()` called. **Enabled by default**, no sandbox, no allowlist, no confirmation. Triggered lazily by read-shaped tools (`list_workflows`, `detect_drift`, `allocate_budget`), so a user who believes they are only reading gets code execution. Pointing an agent at an untrusted repository executes that repository's code with the user's privileges. `SECURITY.md:15` states *"It does not execute user code."* **Proposed fix:** Off unless explicitly enabled (`VERIS_ENABLE_PLUGINS=1` or `--allow-plugins`). On load, print the absolute path and SHA-256 of each plugin file. Never auto-load from an analyzed repo without opt-in. Correct `SECURITY.md` to describe the real behaviour and threat model. **Status:** FIXED — execution requires `--allow-plugins` or `VERIS_ENABLE_PLUGINS=1`; discovered-but-not-executed plugins are reported with SHA-256; executed plugins are disclosed before running. SECURITY.md corrected, with the prior behaviour disclosed. Verified: `PluginLoader.test.ts` 'does not execute a plugin by default' (asserts on a marker file the plugin writes, so it cannot pass if the plugin ran). --- ### D2 · The evaluated agent writes — and can erase — its own evidence · S1 **Where:** [`src/persistence/VerisState.ts:83,183`](../../src/persistence/VerisState.ts) · [`src/mcp/McpServer.ts:333-351`](../../src/mcp/McpServer.ts) **What happens:** `report_execution` accepts `pass`/`fail`/`flaky`/`skipped` from any MCP caller — in practice the same agent whose work is being assessed. The write is `INSERT OR REPLACE` on `(run_id, node_id, tier)` with one run ID per process, so re-reporting the same target **overwrites the earlier row**. An agent can post `fail`, then post `pass`, and the failure is gone. That is a working tamper primitive, not merely an unvalidated input. **Proposed fix:** Append-only rows; never replace. Every row carries a producer identity and a trust class — `veris-derived` (computed), `harness-observed` (external runner), `agent-asserted` (claimed). Confidence and any gate consume trust classes separately; an `agent-asserted` claim alone can never raise assurance. Retain the full history and expose it, so contradiction is visible rather than silently resolved. **Status:** FIXED — evidence is append-only and hash-chained with producer identity and trust class; coverage weights `agent-asserted` at half and lets a more-trusted failure override a less-trusted later pass. Verified: `VerisState.test.ts` 'retains an earlier failure when a later pass is posted', 'detects a row edited directly in the database', 'detects a deleted row'; `ConfidenceEngine.test.ts` 'does not let a later agent-asserted pass bury a harness-observed failure'. --- ### D3 · Dashboard loads unpinned third-party script over the network · S2 **Where:** [`src/reporting/ReportingEngine.ts:199`](../../src/reporting/ReportingEngine.ts) · [`README.md:85,115`](../../README.md) **What happens:** The generated HTML includes `