An example plugin for compprov-analytics,
showing how to extend it without modifying its code: a domain-specific type wrapper
(ComputationEnvironment customization) and a custom LLM chat model, both discovered at
runtime via java.util.ServiceLoader.
If you're looking for the core provenance-tracking framework itself, see compprov-core.
- How plugins work
- What this plugin provides
- Building
- Using the plugin
- Test snapshots
- Sample analysis run
- Project layout
- License
compprov-analytics accepts one or more --plugin=<path-to-jar> command-line arguments. For
each plugin jar, it opens a URLClassLoader over the jar and asks ServiceLoader for two kinds
of providers declared under META-INF/services:
| Service interface | Purpose |
|---|---|
io.compprov.core.EnvironmentCustomizer |
Given the shared ComputationEnvironment, register additional type wrappers, serializers, etc. |
dev.langchain4j.model.chat.ChatModel |
Supplies the LLM used for prompt-based analysis of a CPG. |
Any provider found is applied automatically — no other wiring is required. compprov-core and
langchain4j-open-ai are declared with provided scope in pom.xml because they're
already on the classpath compprov-analytics loads plugins with, so this jar doesn't bundle
them. langchain4j-anthropic is not provided by the host, so it's declared as a normal
compile dependency and shaded into this jar at build time (see Building).
io.compprov.examples.nav is a small, framework-agnostic domain model for a multi-currency
portfolio, including a simple vanilla-options position type:
Amount— aCurrencyplus aBigDecimal, scaled to that currency's decimal precision and always rounded down. Truncation is deliberate: when paying out of a fixed balance, rounding a debit up could subtract more than is actually available and drive the balance negative, so everyAmountoperation truncates rather than rounds to guarantee that never happens.Rate— an exchange rate between two currencies, used byAmount.convert(Rate).Currency—BTC,WBTC,ETH,USDC,USDT,USD,EUR,WSTETH, each with its own decimal precision.WBTCis tracked separately fromBTCand only ever converted between the two via an explicit pegRate, never treated as interchangeable.OptionPosition— a vanilla call/put position (seeOptionType) with a strikeRateand notionalAmount, computing its own payout at a given settlement price.
io.compprov.examples.nav.wrapped integrates these types with compprov-core without changing
them, following the three-step "custom type wrapper" pattern (see the "Extending with custom
type wrappers" section of the compprov-core README for the full walkthrough):
WrappedAmount/WrappedRate/WrappedOptionPositionextendAbstractWrappedVariable<T>and declare their operations (add,subtract,scale,convert, variadicaddBulk,payout) asDescriptorconstants mapped to computation lambdas.AmountWrapper/RateWrapper/OptionPositionWrapperimplementVariableWrapper<T>, the one-method factory the framework calls to instantiate tracked variables.AmountDeserializeris a Jackson 3.x (tools.jackson.*) deserializer registered alongsideAmountWrapperso thatAmountvalues round-trip through a CPG snapshot's JSON.RateandOptionPositionare records, so Jackson deserializes them without a custom deserializer.
NavEnvironmentCustomizer
ties it together — it's the EnvironmentCustomizer that registers all three wrappers on the
shared ComputationEnvironment, and it's the class named in
META-INF/services/io.compprov.core.EnvironmentCustomizer.
EnvVarConfiguredChatModel
is a langchain4j ChatModel created using environment variables — CHATMODEL_URL,
CHATMODEL_API_KEY, CHATMODEL_NAME — and it's the class named in
META-INF/services/dev.langchain4j.model.chat.ChatModel.
It builds one of two underlying clients depending on CHATMODEL_URL:
If CHATMODEL_URL contains "anthropic" |
Otherwise |
|---|---|
Uses langchain4j's native AnthropicChatModel against the real Claude Messages API. |
Uses OpenAiChatModel against an OpenAI-compatible chat completions endpoint. |
Real JSON-schema-enforced structured output (risk_score/markdown_report), server-side system-message caching (cacheSystemMessages(true)), a 5-minute request timeout, a 100k output token budget. |
Requests "json_object" mode (a generic JSON object, not a schema-enforced one) and the same token/timeout settings. |
For Claude, this means pointing the three env vars at Anthropic's native API — not its OpenAI-compatibility endpoint:
| Env var | Value |
|---|---|
CHATMODEL_URL |
https://api.anthropic.com/v1/ |
CHATMODEL_API_KEY |
your Claude API key |
CHATMODEL_NAME |
a Claude model ID, e.g. claude-sonnet-5, claude-opus-5, claude-haiku-4-5 |
Anthropic's OpenAI-compatibility endpoint (same base URL, but built for the OpenAI SDK) is
deliberately not used here: it's meant for evaluating model capabilities rather than
production use, and it silently ignores response_format and prompt caching — exactly the two
things this plugin relies on. Going through langchain4j's native AnthropicChatModel instead
gets both for real.
You don't have to build this yourself — every GitHub release has the built jar
(compprov-plugin-example-<version>.jar) attached, produced by the
Release workflow. Download it and skip straight to
Using the plugin.
To build from source instead, you'll need Java 17+ and Maven. compprov-core must be available
in your local Maven repository (build it from
compprov-core first, or depend on a published
version):
mvn packageThis produces target/compprov-plugin-example-0.1.0.jar containing this project's own compiled
classes, META-INF/services entries, and langchain4j-anthropic (needed for the native Claude
integration, and not provided by the host). compprov-core and langchain4j-open-ai — along
with langchain4j-anthropic's shared transitive dependencies (langchain4j-core,
langchain4j-http-client*) — are excluded from the shaded jar because the host already provides
them at runtime; see the maven-shade-plugin configuration in pom.xml.
Pass the built jar to compprov-analytics on the command line:
export CHATMODEL_URL=https://api.anthropic.com/v1/ # required by EnvVarConfiguredChatModel
export CHATMODEL_API_KEY=your-claude-api-key # required by EnvVarConfiguredChatModel
export CHATMODEL_NAME=claude-sonnet-5 # required by EnvVarConfiguredChatModel
java -jar compprov-analytics.jar \
--cpgpath=path/to/snapshot.json \
--plugin=target/compprov-plugin-example-0.1.0.jarcompprov-analytics logs each discovered provider (Applying customizer: ...,
Using chat model) on startup. From then on, any CPG snapshot containing Amount, Rate, or
OptionPosition input values deserializes and replays correctly, and prompt-based analysis uses
the model at CHATMODEL_URL/CHATMODEL_NAME — Claude, in the example above.
samples/snapshots/ holds the CPG fixtures used to exercise compprov-analytics' fraud-pattern
prompts, split into normal/ (should audit clean) and malicious/ (should trigger a detection).
Every fixture is generated by an @Test-annotated class under src/test/java/io/compprov/examples/
— these aren't real assertions-based unit tests, they're snapshot generators: each one builds a
calculation (one per domain — NAV, payroll, insurance claims, loan amortization, e-commerce
orders, plus a batch of other business and physics/chemistry/engineering domains), prints its
provenance graph via ComputationEnvironment.toJson(...), and the printed JSON is what gets saved
as the corresponding fixture. Run one directly with, e.g.:
mvn test -Dtest=io.compprov.examples.payroll.PayrollCalculatorOne file per domain, 33 in total (payroll.json, insurance.json, idealgas.json, ...), named
after the domain's test package — business domains (payroll, insurance, loans, e-commerce,
construction, subscriptions, ...) alongside physics/chemistry/engineering ones (ideal gas law,
free fall, orbital period, pendulum period, heat conduction, solution dilution, ...). Every
calculation here is deliberately correct, with no injected defects, to serve as true-negative
fixtures. The three NAV scenarios keep their original names (profit.json, payout.json,
net_asset_value.json). metrology.json is the one exception: it comes from a compprov-core
test case rather than this project, so there's no local generator for it.
Organized into one subfolder per fraud-detection prompt compprov-analytics runs, each holding one
malicious sibling per domain that supports it (same domain-based filename as normal/):
| Subfolder | Attack | Generator suffix |
|---|---|---|
calculation_omission/ |
A mandatory debit/credit entry is computed correctly, then dropped from the final aggregation | *Omission |
lineage_disconnection/ |
A correctly-derived value is swapped for a disconnected literal with no edge back to its root inputs | *LineageDisconnection |
precision_tampering/ |
One operation in a chain silently uses a different MathContext/rounding than its neighbors |
*PrecisionTampering (NAV: PayoutCalculatorSlicing) |
semantic_tamper/ |
A value's descriptor label contradicts how it's actually used/classified in the graph | *SemanticTamper |
double_counting/ |
One value is fed into an aggregation through two parallel paths | *DoubleCounting |
Each generator is the same scenario as its normal/ counterpart with one deliberate change — a
commented-out subtraction, a disconnected literal, a mismatched MathContext, a mislabeled value,
a duplicated aggregation path — to inject exactly the fraud pattern that subfolder is meant to
test. Not every normal/ domain has a malicious sibling in every subfolder — only the ones whose
calculation shape actually supports that attack (e.g. double_counting/ needs an aggregation with
a value reachable through two paths, which not every domain has).
malicious/legacy/ holds two fixtures that predate the per-domain generators and have no
corresponding @Test class in this repo — broken-chronology.json and
nav_invalid_calculation.json (an early NAV fixture with a deliberately corrupted aggregate that
doesn't replay from its own recorded operations) — kept for reference but not regenerated by
anything here.
Two more fixture sets sit alongside normal//malicious/ rather than under them, each
reimplementing attack patterns from a published smart-contract auditing tool as a CPG:
gptscan/— five DeFi lending/vault scenarios (collateral valuation, governance vote weight, interest accrual distribution, transfer checkpoint, vault first deposit) generated by*SemanticTypeAttack-suffixed classes undersrc/test/java/io/compprov/examples/gptscan/. Named after GPTScan, a logic-bug-detection tool for smart contracts; each fixture reproduces one of its bug patterns, e.g.CollateralValuationCalculatorSemanticTypeAttackpricing collateral off raw, single-block AMM pool reserves instead of a verified oracle.numscout/— six AMM/staking scenarios generated by classes undersrc/test/java/io/compprov/examples/numscout/: one correct baseline (GameWinnerPayoutCalculator) plus five*PrecisionTamperingvariants (AMM taker fee, liquidity pool join, staking reward fee, token sale threshold, and a tampered variant of the baseline itself). Named after the NumScout paper's precision-loss taxonomy — each fixture injects one specific rounding-direction defect (e.g. floor division where the protocol requires ceiling division) atBigIntegerwei-scale rather than theBigDecimalscale the other fixtures use.
These use DefaultComputationContext/BigInteger wrapping directly rather than this plugin's NAV
domain types, so they don't exercise the plugin's EnvironmentCustomizer — they're here as
additional fraud-pattern coverage for compprov-analytics itself.
samples/outputs/llm-api/sonnet-5/ holds compprov-analytics --executePrompts=true runs (Claude
Sonnet 5), one subtree per fixture category — normal/, malicious/, gptscan/, numscout/ —
each under its own <timestamp>/out/, with one subfolder per input file plus a top-level
summary.md. samples/outputs/llm-manual/ holds one additional run spanning the entire combined
83-file fixture set (normal/ + malicious/ + gptscan/ + numscout/) in prompt-dump mode:
instead of calling the configured chat model, compprov-analytics writes each prompt to a
*_prompt.md file for pasting into a chat UI by hand, so its summary.md has no verdicts (— in
every alarm column) — a way to exercise this plugin without needing CHATMODEL_API_KEY set.
Run under the three-prompt default (topological_fraud, precision_tampering,
semantic_type_and_context_cast_attack):
normal/(33 fixtures) — 32 come back fullyCLEAN. The one exception ismetrology.json, flagged by precision tampering (SUSPICIOUS LOGIC, risk score 32): of its 56 operations, 55 explicitly thread a shared 34-digitMathContext— the one exception is a saturation-vapor-pressure exponentiation (Exp_double) that has no arbitrary-precisionexp()available and so drops to nativedouble. The model correctly spots this as the only operation missing themcargument, but also recomputes that the resulting error is many orders of magnitude below both the final result's precision and the input measurements' own uncertainty — a real, technically-necessary gap rather than an injected defect.malicious/(39 fixtures) — 38 trip at least one of the three prompts (not always the alarm matching their own category, e.g. alineage_disconnection/fixture caught bytopological_fraudinstead). The one miss,legacy/broken-chronology.json, comes backCLEANon all three prompts — it's only caught by compprov-analytics' separate chronology-validity check, not by the LLM prompts.gptscan/(5 fixtures,semantic_type_and_context_cast_attackonly) — all 5 flagged,ANOMALY DETECTEDat risk scores 74–88, reimplementing bug patterns from GPTScan.numscout/(5 fixtures,precision_tamperingonly) — all 5 flagged (2ANOMALY DETECTED, 3 lower-confidenceSUSPICIOUS LOGIC), reimplementing defect patterns from the NumScout taxonomy. No misses in this run.
src/main/java/io/compprov/examples/nav/
├── model/ Plain domain types: Amount, Currency, Rate, OptionPosition, OptionType
└── wrapped/ compprov-core integration: WrappedAmount, WrappedRate, WrappedOptionPosition,
AmountWrapper, RateWrapper, OptionPositionWrapper, AmountDeserializer
src/main/java/io/compprov/plugin/example/nav/
├── NavEnvironmentCustomizer.java EnvironmentCustomizer SPI provider
└── EnvVarConfiguredChatModel.java ChatModel SPI provider
src/main/resources/META-INF/services/
├── io.compprov.core.EnvironmentCustomizer
└── dev.langchain4j.model.chat.ChatModel
Apache License 2.0 — see LICENSE.