CE-RISE Intelligent Circular Insights Workbench
CE-RISE Intelligent Circular Insights is a workbench for querying, validating, repairing and generating Digital Product Passport (DPP) records. Answers cite supporting evidence or are declined when support is insufficient. Record generation uses supplied facts and retrieved evidence; unverified repair suggestions remain separate for human review.
What you can do
- Query product records and inspect the evidence cited in an answer.
- Validate records against supported DPP and CE-RISE model profiles.
- Repair fields where evidence supports a change, with unverified suggestions kept separate for review.
- Generate a record from supplied facts and retrieved evidence, with field-level support and a check against the applied validation profile.
Get started
The run-from-source guide covers local use and a long-running installation.
Work with records
Follow the query, validate, repair and generate workflows in the browser interface. Backends and evidence explains which sources support each result.
Engineering reference
Architecture describes the components and their interfaces. Testing covers the verification approach, and the decision records explain design choices.
Citation
Use the Zenodo concept DOI 10.5281/zenodo.23257806 to cite the workbench across all versions.
Licensing
See the licensing decision for the software and vendored data-model terms.
Funded by the European Union under Grant Agreement No. 101092281 — CE-RISE. Views and opinions expressed are those of the authors only and do not necessarily reflect those of the European Union or the granting authority (HADEA).
Run from source
The workbench has a Python API and a browser interface. These instructions use
the source repository; they do not require a container image. Run the Python
commands from the repository root so the application can find its included data,
schemas and .env file.
Local use
Install Python 3.10-3.12, uv and Node.js 20 or newer. From the repository root, install the dependencies:
make setup
make web
Start the API in one terminal:
make demo
Start the browser interface in another:
cd apps/web
npm run dev -- --port 5173
Open http://localhost:5173. The development server forwards /api requests
to the API at http://127.0.0.1:8000; check its status at
http://127.0.0.1:8000/api/health. make demo enables automatic reload and is
for local development, not a long-running installation.
The default LLM_CASSETTE_MODE=replay does not contact a model provider. Features
that need a model response may have no matching recorded response; validation and
other deterministic operations remain available. To use live model responses,
copy .env.example to .env, set LLM_CASSETTE_MODE=live and supply
OPENAI_API_KEY, then restart the API. Keep .env out of version control.
Long-running installation
This example uses a Linux host with systemd and Nginx. Install Python 3.10-3.12,
uv, Node.js 20 or newer, and Nginx. Check out a chosen repository revision in a
stable location accessible to the service account. In the commands and examples
below, replace /srv/intelligent-circular-insights with that location and ici
with the service account.
As the service account, run from the repository root:
uv sync --locked --all-packages --no-dev
make web
cd apps/web
npm run build
The frontend build is in apps/web/dist. make web installs the frontend
dependencies outside the repository and links them into apps/web/node_modules.
Return to the repository root before starting the API. If live model responses
are needed, create .env there from .env.example, set
LLM_CASSETTE_MODE=live and OPENAI_API_KEY, and restrict the file to the
service account. Without these settings, the API stays in offline replay mode.
For example, install this systemd unit as
/etc/systemd/system/intelligent-circular-insights.service:
[Unit]
Description=Intelligent Circular Insights API
After=network.target
[Service]
Type=simple
User=ici
WorkingDirectory=/srv/intelligent-circular-insights
ExecStart=/srv/intelligent-circular-insights/.venv/bin/uvicorn apps.api.main:app --host 127.0.0.1 --port 8000
Restart=on-failure
[Install]
WantedBy=multi-user.target
Start it and check the API:
sudo systemctl daemon-reload
sudo systemctl enable --now intelligent-circular-insights
curl http://127.0.0.1:8000/api/health
Serve the built frontend and forward /api to the API on the same host. An
Nginx server block can use the following locations (replace the host and root):
server {
listen 80;
server_name insights.example.org;
root /srv/intelligent-circular-insights/apps/web/dist;
index index.html;
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
try_files $uri $uri/ /index.html;
}
}
The /api/ prefix must be preserved when proxying. Configure HTTPS and an
appropriate access policy before accepting private records or exposing this
installation beyond a trusted network. After changing source, sync dependencies
again if the lockfile changed, rebuild the frontend if its source changed, and
restart the API. Check the public /api/health route and load the browser
interface after each update.
Work with passport records
Open the workbench in your browser. The Settings menu lets you choose an available backend; the badge in the header shows which one served a request. The screens below use the backend and evidence available in that installation. See Backends and evidence for what each backend can use and how to read the support behind a result.
Query
Use Search & Answer to ask about the records and evidence already available to the workbench. Enter a question, optionally scope it to a product ID, and select Ask. Read the answer alongside its cited evidence and audit details. The workbench may decline to answer when the available evidence does not support one; the reason is shown with the result.
To ask about a document you have rather than the mounted evidence, use
Single passport. Paste text or JSON, or choose a .txt, .md or .json
file. Select Read the document to inspect the sections found, then ask a
question. This path answers from the supplied document alone; it does not add
the document to the searchable corpus.
Validate
Open Validate, select a profile and paste a JSON record. You can also start with one of the sample records. Select Check conformance to see the applied profile and any violations, including their locations and expected values.
The profile determines what the report means. The EU DPP profile checks required fields and other rules in that profile. A CE-RISE data-model profile checks vocabulary and types but does not establish record completeness. The CE-RISE auto profile routes a recognised CE-RISE document to its model and other records to the EU DPP profile. Check the profile named in the report before interpreting a “conforms” result.
Repair
After a record fails validation, select Repair with evidence on the same screen. The result shows fields filled from same-product sources, unresolved fields, and a repaired JSON preview. Inspect the source reference and pointer shown for each fill, then check whether the repaired record conforms to the selected profile.
The Also list what the model would guess option displays additional suggestions for human review. These are separate from the repaired record: they are not applied or counted as evidence. Turn the option off to request only sourced fills. If no source supports a missing value, the gap remains open.
Generate
Open Synthesize, choose a target profile and enter the facts you already have as JSON in Seed facts. Select Build the passport. A successful result displays the generated record, the profile it was checked against, and field-level support identifying the source for each generated field. If the workbench cannot ground and validate the record, it declines the request and shows why instead of returning a partial record.
Query answers, repair and generation may need model assistance. The default replay configuration can only use recorded model responses; for new live requests, configure the model as described in Run from source.
Backends and evidence
The workbench has two backend choices, Normal and CE-RISE. Choose among the backends available in Settings. The badge in the header reports the backend serving your current session requests, which can differ from the one requested if the latter is unavailable. A warning appears when the backend falls back. The Compare backends view makes separate requests to both backends and does not change that badge.
What each backend uses
Normal searches the documents included with the installation, uses product-scoped facts held in memory during the current process, and offers validation profiles. Its CE-RISE model catalogue describes data models; the catalogue itself is not a collection of product records.
CE-RISE keeps those capabilities and adds structured facts from its mounted study graph. It can use those facts when they cover the subject of a request; selecting this backend does not make the graph a source for every product.
A backend is not a validation profile. In Validate and Synthesize, you can select a named profile independently of the backend. Both backends offer the available CE-RISE data-model profiles as well as the EU DPP profile. With no profile selected, Normal uses the EU DPP profile. CE-RISE tries to match a record to a recognised CE-RISE document model and otherwise uses the EU DPP profile. The result names the profile actually applied. CE-RISE data-model profiles check vocabulary and types; the EU DPP profile checks its required fields and other rules.
Reading the support for a result
- Search & Answer shows the evidence used for an answer and its audit details. If it declines to answer, read the stated reason and weak signal.
- Single passport uses only the document supplied with that request, not the mounted search corpus. Its result identifies the document sections used.
- Repair names the source and pointer behind each filled field. Missing fields without source support stay open; optional model suggestions appear separately and are not applied.
- Synthesize shows a source reference and pointer for each supported field in a returned record. It does not return a record when the requested result cannot be grounded and validated.
Explore the CE-RISE models
Open CE-RISE Models to browse the model catalogue, including each model’s layer, summary and link to its source. In Route a question, enter a topic and select Route to see which model catalogue keywords match its terms. This identifies potentially relevant models; it does not answer the question or check a product record. Mounted substrates shows per-substrate runtime counters, including questions seen and times fired. These counters depend on requests made during the running process; an unlabelled precision value is not an evaluation result.
Compare the backends
Open Compare backends and select one of its preset probes. The screen sends the same request to Normal and CE-RISE and displays the results side by side. The presets cover search, validation and two example impact calculations; they do not accept a user-supplied record or question. Compare the returned values, declines and reported profiles for those examples, rather than treating a matching or differing result as a general claim about either backend. These comparison requests do not change your session’s backend selection.
For the steps on each screen, see Work with passport records. For how the backend assembles and routes its sources, see Architecture.
Architecture
Intelligent Circular Insights has a React/TypeScript browser interface and a
FastAPI service. The API composes domain use cases with adapters for evidence,
data models, validation, reliability and model assistance. The source runs as
one Python service; the browser calls its /api routes.
flowchart LR
WEB[Browser interface] --> API[FastAPI routes]
API --> CORE[Domain use cases and ports]
CORE --> EVID[Evidence and memory adapters]
CORE --> SUB[Data-model and graph adapters]
CORE --> LLM[Model and grounding adapters]
CORE --> REL[Reliability and policy adapters]
Code boundaries
| Location | Responsibility |
|---|---|
apps/web | Browser screens, API client and presentation of evidence, validation and traces |
apps/api | HTTP routes, settings, backend resolution and bundle construction |
packages/ici_core | Domain types, ports and use cases |
packages/ici_evidence | Document retrieval and in-process fact memory |
packages/ici_substrates | Model catalogue, JSON Schema profiles, study graph and impact adapters |
packages/ici_symbolic | Rule-based validation and derivation |
packages/ici_llm | Model providers, request budgets, cassettes and grounding checks |
packages/ici_reliability, ici_policy, ici_datatrust, ici_eval | Confidence and policy components, data-trust interface and evaluation code |
The ici_core package depends on ports, not concrete adapters. The API builds
provider bundles at startup in apps/api/bundles.py; its routes obtain the
selected bundle through apps/api/deps.py. The dependency direction is checked
by the repository’s import-linter contract.
Backend resolution
Requests can select normal or ce-rise with X-Backend-Mode. The API resolves
that choice against enabled bundles and returns the served mode in
X-Backend-Mode-Used. An unknown or disabled choice falls back to the configured
default with a warning header. The browser displays the served mode, not merely
the requested preference.
The Normal bundle searches the included document corpus, keeps product-scoped facts in process memory, and offers the EU DPP and available CE-RISE JSON Schema profiles. The CE-RISE bundle builds on Normal and adds structured facts from a mounted study graph. Its default record profile routes a recognised CE-RISE document to the corresponding model and other documents to the EU DPP profile. Named profiles are respected in either backend. The model catalogue describes schemas and is not itself a source of product-instance facts.
The backend switch also routes impact requests to an engine that declares coverage for the requested subject. These example calculations have their own sources and functional units; they are not part of the passport validation or generation workflow.
Record and answer paths
Query: AnswerQuestion retrieves bounded evidence, uses applicable
structured facts and rules, composes an answer, checks grounding, and returns
either an answer with provenance or a stated abstention. Single passport
uses the same reliability path with only the document supplied for that request.
Validate: ValidateRecord checks a JSON record against the selected
profile and reports typed violations with locations. The EU DPP profile checks
its required fields; CE-RISE model profiles check vocabulary and types but have
no required root fields.
Repair and generate: SynthesizeRecord uses supplied facts and retrievable
record evidence. Repair returns sourced fills separately from unresolved fields
and optional, unapplied model suggestions. Generation returns a record only
after field-level grounding and validation against the applied profile.
These operations use the same request-scoped model controls and audit trace.
LLM_CASSETTE_MODE=replay is the default; it makes no provider call and may
decline when no recorded response matches. Validation and other deterministic
operations do not require a model key.
State and deployment
The shipped fact memory is in process and is not restored on restart. The API
reads configuration from environment variables or .env in the repository
root. Included corpora, schemas and graph data live under data/, schemas/
and ontology/; the API must run with the repository as its working directory.
The frontend uses same-origin /api calls in a deployed installation and a
Vite proxy during local development. See Run from source
for both setups and Testing for verification commands.
The original, more extensive architecture design record is retained for its rationale and research directions. Proposed features in that record should not be read as current capabilities.
Testing
The Python suite checks domain behavior, adapter contracts, HTTP routes, grounding, and both backend modes. Browser smoke tests exercise the interface against an API started in cassette replay mode. The commands below run from the repository root after source setup.
Local checks
| Command | What it runs |
|---|---|
make check | Ruff lint and format checks, MyPy and import-linter contracts |
make test-fast | Python unit and contract tests |
make test | Full Python suite, excluding opt-in live tests |
make web-check | Frontend typecheck and production build |
make smoke | Playwright browser tests with both servers started automatically |
make secrets | Tests that tracked files contain no credentials |
make smoke needs a Chromium browser. Playwright can install one, or
ICI_CHROMIUM can point to an existing executable. Its servers use dedicated
local ports (18000 for the API and 15173 for the browser) so they do not
reuse a developer’s live-model session.
Offline model tests
The default model mode is LLM_CASSETTE_MODE=replay. Recorded responses in
tests/cassettes/recorded make model-dependent tests reproducible without an
API key. A missing cassette fails rather than making a paid call. The ordinary
pytest fixture removes OPENAI_API_KEY and blocks network access. Live tests
are deselected unless explicitly requested; -m live alone does not enable
them.
The live-model check is a separate, deliberate operation:
make live
It needs an API key, can make paid requests, and is rejected by the test
configuration in CI or alongside --no-network. See
tests/live/README.md for its request and spending
limits. Offline success does not establish live-model output quality.
Test coverage
| Area | Tests |
|---|---|
tests/unit | Domain invariants, retrieval and memory, schema handling, reliability, model runtime and deterministic calculations |
tests/contract | Shared assertions against implementations of domain ports |
tests/integration | Bundle construction and adapter interaction |
tests/e2e | HTTP behavior, profile selection, record assistance and backend switching |
tests/golden and tests/reference | Regression cases against captured reference behavior |
apps/web/tests | Browser interactions and async UI state in both backend modes |
tests/live | Opt-in provider checks using synthetic or public inputs |
The response envelope enforces that answered prose is grounded and carries provenance, and that calibrated confidence clears the request’s operating threshold. Unit and end-to-end tests cover these invariants and the explicit abstention path. Validation and record-assistance tests distinguish the profile that actually ran, sourced fills, unresolved gaps and unapplied suggestions.
Continuous integration
The Forgejo workflow in .forgejo/workflows/ci.yml runs on pushes, pull
requests and manual dispatch. Its Python 3.12 job runs the full code-quality
gate and offline Python suite with coverage. A Python 3.10 job runs typechecking
and the unit and contract subsets. Neither job receives an API key. Frontend
build and Playwright smoke tests are local checks, not part of this workflow.
The 22 September 2026 verification checkpoint records results from one earlier local run; it is not a claim about the current revision. The original testing design record is retained for rationale and proposed coverage, not as a list of tests that now run.
ADR 0001 — Hexagonal architecture with Protocol ports
Status: accepted · Date: 2026-09-19 · Sprint: 0
Context
The current CE-RISE-Demo mixes I/O, configuration reads and business logic in the same
modules (demo_search.py 1371 LoC, carbon_calculation_service.py 1343 LoC). We must
support two interchangeable backends and publish the result under a DOI.
Decision
Ports-and-adapters. ici_core holds domain types, use cases and eight ports declared as
typing.Protocol. Adapter packages implement them. apps/api is the only module that
imports both adapter packages.
Protocols, not ABCs: structural typing means an adapter never imports a base class, a test double is a plain dataclass, and there is no inheritance coupling across package boundaries.
Consequences
- Mode switching becomes an adapter swap, not a branch through feature code.
- One contract suite validates both implementations.
ici_coreis testable with no I/O, so a 95 % coverage floor is reasonable there. − More files, more indirection; a reader must follow a port to find the implementation. − Port signatures must be got right early; changing one after Sprint 0 needs an ADR.
Enforcement
import-linter contract in CI: ici_core may not import any adapter package. Violations
fail the build.
ADR 0002 — Backend mode is a per-request adapter swap
Status: accepted · Date: 2026-09-19 · Sprint: 0
Context
We need Normal mode (flat profiles, CSV factors, JSON Schema) and CE-RISE mode (PEFDPP graph, CE-RISE data models) available in the same deployment. SHACL conformance checking may be added to CE-RISE mode in a future development. Options considered: a startup env profile, per-feature overrides, or per-request resolution.
Decision
Per-request, via an X-Backend-Mode header, mirroring the existing X-Model convention
that the Settings panel already uses. Precedence: header → session pin → settings default →
env → hard default normal. Both ProviderBundles are constructed once at startup and
held immutably; resolution is a dict lookup.
Every response carries X-Backend-Mode-Used, and AnswerEnvelope.mode names the backend
that actually ran.
Consequences
- Both modes live in one session — the compare view is possible, which is the demo.
- A/B and differential testing need one process, not two.
- No restart to switch; no configuration drift between two deployments. − Both bundles’ memory footprint is resident, including the PEFDPP graph (~5,400 triples, cheap) but excluding the 40 MB EF flow list, which stays lazy. − Every use case must be written mode-agnostic; a use case that reads a mode flag is a design failure and is rejected in review.
Rejected
A startup env profile: simpler, but it makes the compare view impossible and forces two processes for any A/B measurement — which is most of what the papers need.
ADR 0003 — One reliability envelope, with three enforced invariants
Status: accepted · Date: 2026-09-19 · Sprint: 0
Context
COMPASS’s central claim is that every output is either an evidence-grounded answer with provenance or an explicit abstention. PEFDPP’s central claim is reproducibility down to the triple. Both are properties of the response, so the response type should enforce them.
Decision
One ReliabilityEnvelope for every use case in every mode:
decision · answer · value · evidence · provenance · confidence · data_trust · operating_point · grounding · mode · trace.
ProvenanceLink.kind ∈ {PASSAGE, FACT, RULE, TRIPLE, CALC_STEP}. CE-RISE mode is
distinguished by emitting TRIPLE links carrying the IRI and the TTL file it lives in.
Three invariants, enforced at construction and asserted in every contract and e2e test:
ANSWER ⟹ len(provenance) > 0— COMPASS’s reliability claim.ANSWER ⟹ grounding.verdict == FULLY_GROUNDED— the enforced form of evidence-before-generation (§4.1 says it is currently prompting, not guarantee).ANSWER ⟹ confidence.calibrated ≥ operating_point.tau, with τ returned in the response, so the operating point that produced an answer is auditable.
No configuration flag relaxes any of them.
Consequences
- The frontend renders one component tree for all six windows and both modes.
- The papers’ reliability claims become type invariants rather than conventions.
data_trustandoperating_pointgive the bias-aware work a place to land without a fork.- Regressions in grounding surface as test failures, not as quietly worse answers. − Every adapter must produce provenance even where the current code returns a bare string; this is real porting work in Sprint 1.
ADR 0004 — Capture golden responses from the running demo before writing any adapter
Status: accepted · Date: 2026-09-19 · Sprint: 1
Context
The instruction is that current features must not break. The current behaviour is under-documented and under-tested (9 test files across the two backends, ~20,200 LoC), so “don’t break it” cannot be verified by reading.
Decision
Before the first adapter is written, tooling/capture_golden.py drives the live
CE-RISE-Demo across every endpoint, every example DPP and the three broken ones, and records
the responses to tests/golden/normal/. Normal mode must reproduce them byte-for-byte,
normalising only timestamps, correlation ids, latency fields and float noise beyond
published precision.
The PEFDPP physics values are locked the same way in tests/golden/pefdpp/.
Consequences
- “No regression” becomes a command that exits 0 or 1.
- Behaviour nobody documented is captured anyway. − Golden tests also lock in current bugs. Any intentional fix must update a golden file in the same commit, with the reason in the message — which is the point: a deliberate change is visible in review.
ADR 0005 — Knowledge substrates are mounted, and their contribution is measured
Status: accepted · Date: 2026-09-19 · Sprint: 2 Supersedes: the first draft’s “vendor the CE-RISE models” ADR, which treated one substrate as the architecture.
Context
COMPASS’s symbolic layer fires on 7.96 % of the workload with observed conditional precision 1.000. The precision is the contribution; the coverage is the limitation. Several bodies of structured knowledge could raise coverage — the DPP core ontology and its domain modules, the 17 CE-RISE data models, the PEFDPP graph, and an unseen schema like Open Food Facts — but each raises a different question about whether precision survives.
An architecture that hard-wires one of them cannot answer that question, and an architecture that hard-wires the largest one confuses a substrate with a design.
Decision
SubstrateRegistry mounts substrates. Each declares the subjects it covers, the facts it
supplies and the queries it accepts. Crucially it exposes coverage_report(): symbolic fire
rate and conditional precision per substrate.
A mode is a named bundle of substrates plus adapters plus a default operating point. “CE-RISE mode” mounts the CE-RISE data models and the PEFDPP graph on top of the Normal set.
CE-RISE model schemas are vendored into schemas/ce-rise/ with source URL, commit SHA and
fetch date in VENDOR.md; the application never fetches a schema at runtime. Codeberg is
unreachable from the build sandboxes (403 at the egress proxy), and a DOI-archived artifact
must validate years later exactly as it did on release day.
Consequences
- “Which knowledge bought which reach” becomes a number in a report.
- The OFF generalisation study runs from the same registry as everything else, rather than a separate script.
- A substrate that raises coverage but drops conditional precision below the published bar is visibly not worth mounting — which is a finding, not a failure.
- Reproducible, offline builds. − Vendored copies go stale; a scheduled job diffs upstream and opens an issue. − One more layer of indirection between a question and the triple that answers it.
ADR 0006 — The model selects parameterised queries; it does not write them
Status: accepted · Date: 2026-09-19 · Sprint: 2–3
Context
When a substrate is a graph, the obvious implementation is to let the model write SPARQL. That reintroduces the hallucination surface the whole architecture exists to remove, and opens an injection surface on an endpoint that must stay read-only.
Decision
A parameterised template library (ontology/cq/templates/*.rq), derived from the live
competency questions. The planner selects a template and binds its parameters through
structured output against a JSON Schema. No template fits → no_template → abstain, with
that as the stated reason.
Consequences
- The query surface is enumerable, reviewable and testable.
- Abstention has a precise, explainable cause — which is what §4.4 asks of every abstention.
- Each added template is a measurable increase in reach. − Questions outside the template set are abstained on rather than attempted. That is the intended trade, and template coverage is a reported number rather than a hidden one.
ADR 0007 — Calibration is a subsystem with a port, not a function in a scoring path
Status: accepted · Date: 2026-09-19 · Sprint: 4
Context
The paper reports 10-bin in-domain ECE 0.525 for COMPASS against 0.487 for RAG-Base
(0.5247 to four decimals in tables/calibration_diagnostics.tex) — the
strongest accuracy configuration is the worse-calibrated one — while the same pipeline
reaches 0.021 on Open Food Facts. The abstract names calibration on the source domains as an
open challenge.
Today the confidence signals (retrieval margin, snippet agreement, symbolic fire flag,
generation probability) are aggregated into a scalar ĉ(x) and then calibrated by a
monotone map. A monotone map of a scalar cannot recover information the collapse destroyed.
Decision
Calibrator becomes a port with at least four implementations: isotonic (current),
Platt/temperature, beta, and a vector calibrator over the full signal vector. Signals are
a named, inspectable SignalVector in the envelope rather than an internal scalar. Fitting
is per domain and per mode, with leave-one-study-out reporting. Diagnostics — ECE (10-bin
and adaptive), Brier decomposed into reliability/resolution/uncertainty, reliability
diagrams, per-signal ablation — are first-class output, not a script.
SelectivePolicy is separated from Calibrator: τ is chosen for a coverage target on a
held-out split and reported in the envelope, because the paper’s position is that operating
points are policy choices.
Scope
This deliverable does not run the calibration experiment. Isotonic ships as the default, because it is what the system does today. The other implementations are present and unfitted. What changes is that swapping one in, or fitting per domain, no longer requires touching the call site.
Consequences
- The leading hypothesis about the in-domain gap — that the scalar collapse discards structure a vector calibrator could use — becomes testable later without a refactor first.
- An abstention can name which signal was weak, because signals are a named field rather than an internal scalar. The UI should show this; today it cannot.
- A data-trust layer landing later feeds the same pipeline rather than a parallel one. − Slightly more indirection on the hot path, for a negligible per-request cost.
ADR 0008 — The policy seat: ship the router, leave room for the rest
Status: accepted · Date: 2026-09-19 · Sprint: 1
Context
The paper states the RL router “is reported only for completeness; its paired tests show no significant improvement and it is not claimed as a contribution.” That is an honest result. It is also explainable: the current agent is a linear softmax over six hand-made features with four actions, trained on a small episode file, with abstention applied downstream as a threshold rather than learned.
Decision
DecisionPolicy is a port. The supervised router ships as the default, because it is the
configuration that works today. Two further seats exist and stay empty in this deliverable:
- a contextual bandit (LinUCB / Thompson) over the confidence signal vector — most of this problem is contextual rather than sequential, and a bandit would come with a regret bound the RL policy does not have
- an offline RL policy with
ABSTAINas a first-class action and rewardr = 1{correct} − λ·1{wrong} + 0·1{abstain},λ > 1, so the risk–coverage trade-off is learned rather than applied afterwards
Two architectural choices make those seats usable later: the policy’s observation is the
confidence signal vector rather than six hand-made features, and ABSTAIN is expressible
as an action rather than only as a downstream threshold.
Scope
No policy evaluation happens in this deliverable. No off-policy estimation, no significance testing. The seven evaluation modes stay runnable with a smoke test each, because they are part of what CE-RISE is being handed.
Consequences
- A later attempt at the policy question starts from the right interface instead of a refactor. The published “no significant improvement” result is worth revisiting, and the changes that would make it worth revisiting are structural.
- The routing gap the paper names (COMPASS 0.7085 vs Sym-Only 0.9051 on logic) is
addressable by a policy that can see the symbolic fire signal — which this observation
space provides.
− Two empty seats in the codebase. They cost a
Protocoland a docstring.
ADR 0009 — Request-scoped LLM audit and explicit prose grounding
Status: accepted for Sprint 0x, 2026-09-19. The user authorized necessary integration fixes.
Context
The frozen LLM ports return strings/mappings and have no trace argument. Model
selection and detailed generation audit still need to reach each response without
mutating the resident per-mode bundles. The envelope also admitted unchecked prose
through NOT_APPLICABLE, and a report could say fully grounded with fewer resolved
claims than its total.
Decision
- Keep every existing port signature unchanged.
LLMRuntime.request(model=...)creates a fresh provider, verifier, budget and audit log.bind(bundle)returns a replaced immutable bundle;finish(envelope)merges audit into the trace.LLMRuntime.answer_question(...)performs both operations for Search. - The core catches the new model-independent
GenerationErrorand returns an explained abstention. Provider-specific refusal, truncation, invalid output, availability and rate-limit errors inherit it. Cassette errors do not: a missing or corrupt fixture must fail a test, never be mistaken for ordinary abstention. - Prose answers require a nonempty, fully resolved grounding report. Only
deterministic numeric-only answers may use
NOT_APPLICABLE. Claim counts must agree with the verdict and unresolved list. - A verifier checks existing citation ids, exact evidence quotes, answer coverage, and numeric support in addition to a model’s entailment judgment. This is not a proof of semantic truth: model judgments and claim decomposition can be wrong.
- No prompt or provider config is read from environment in
ici_llm; the API composition root supplies settings. Prompts are versioned Markdown files and included in built wheels.
Consequences
Feature routes should use the runtime wrapper or the explicit request/bind/finish sequence. Direct calls to the ports still work but do not automatically enrich the envelope’s trace. Later guard/composition work can reuse the request audit without changing core signatures. Current numeric checking is conservative: arithmetic/unit-conversion claims need explicit derived evidence in the pack, otherwise they may abstain.
CostAccount.usd is estimated new spend, zero for replay. Historical token usage,
estimated original cost, prompt/model identity and replay status remain in LLM
trace steps. Unknown pricing/usage is labelled unavailable in those steps; it is
not a certified billing total. Dollar ceilings and retry scheduling remain X9.
ADR 0010 — Distinct licences for software and CE-RISE data models
Status: accepted · Date: 20 September 2026
Context
The software repository uses EUPL-1.2. The CE-RISE data models vendored into it retain their upstream CC-BY-NC-4.0 licence. These are different artefacts with different licence scopes. Their inclusion in one repository does not, by itself, create a conflict or change either licence.
The repository-root LICENSE identifies the software licence. Without a separate
notice, readers could incorrectly assume that it also applies to the vendored
models. Equally, the models’ NC condition must not be presented as a restriction
on all software in the repository.
Decision
- Application code remains under EUPL-1.2, as declared by the repository
LICENSEand source-file SPDX identifiers. - Vendored CE-RISE data models and their generated schema artefacts retain
their upstream CC-BY-NC-4.0 terms. They remain in
schemas/ce-rise/, with a separateLICENSE,NOTICE.md, upstream provenance and file-level licence metadata. The application reads the schemas as data at runtime. - Documentation and metadata identify the applicable licence for each part; neither licence is described as covering the entire checkout.
- This decision does not propose or require a change to either licence. Any future relicensing proposal belongs with the relevant rights holders and would be recorded separately.
Consequences
- Users can identify the applicable terms for the software and for the data models without treating the repository as a single-licence work.
- Keeping vendored models and generated schemas together preserves their attribution and pinned provenance for reproducible validation.
- The NC condition applies to use of the licensed model material; it does not change the licence of the application code. Users planning uses of the models outside the licence terms may need separate permission.
- A separate directory and notice clarify scope. They do not resolve a licence conflict, because co-location alone creates none.
Scope of openness
EUPL-1.2 permits commercial use of the software. CC-BY-NC-4.0 limits uses of the model material to those allowed by its NonCommercial condition. This is a distinction between the permissions attached to different artefacts, not a reason to relicense either one. The project therefore retains the existing licences and labels their scope explicitly.
Not legal advice
This ADR documents the repository’s licence boundaries, not a legal opinion about a particular downstream use. Questions about uses outside the stated terms should be addressed with the relevant rights holders or legal advisers.
ADR 0011 — Separate grounded repairs from unverified suggestions
Status: accepted in Sprint 1, 2026-09-20.
The user explicitly amended the evidence-only repair plan: values drawn from the LLM’s training should also be proposed, with lower confidence. The amendment does not require silently applying guesses or weakening the answer grounding invariant.
RecordComposer.repair() therefore returns two distinct collections:
fills: exact typed values at the same JSON Pointer in supplied, same-product structured evidence, carrying an evidence id, resolvable ref and source pointer. Only eligible schema violations are repaired; valid fields are not overwritten.suggestions: model-training candidates,status=unverified,requires_review=true, confidence capped at 0.3. Confidence is a model feature, not a calibrated probability. These do not changerecord, clear its unresolved violations, become provenance, or feed answer grounding. They may be empty.
Training suggestions are enabled by default and can be disabled with
suggest_from_training=False. Invalid-shaped candidates and invented administrative
ids/dates/schema versions are suppressed. UI acceptance must be an explicit,
auditable user action; it must not silently promote a suggestion to validated evidence.
The separate structured-suggestions system prompt is selected only by the trusted
repair schema’s x-ici-unverified-suggestions annotation. Ordinary structured
generation, synthesis and answer composition remain evidence-only.
Synthesis validates the complete demo EU DPP JSON Schema, including formats and
nested arrays, plus the legacy material-share total bound. Every returned leaf
must resolve to the seed or same-product structured evidence. Failure receives
one retry with violation paths, then RecordGenerationError, not template defaults.
The bundled schema is an unchanged CE-RISE demo schema, not regulatory certification.
Text-only context is not sufficient for an automatically applied record fill.
Intentional differences from legacy behavior:
- No automatically inserted
[needs review], fabricated material percentages, category compliance assertions, numeric clamping, or material normalization. - Fallback hints must have auditable citations and still pass final grounding.
- Recycling checks use the hint’s actual number instead of hard-coded
70. - Numeric hint checks compare complete values, not substrings (
5versus55). - Citation ids are not counted as product/component identifiers.
- Refusals abstain; extraction never bypasses a refusal or the grounding verifier.
Real-response testing exposed ambiguous decomposition coverage instructions. The grounding prompt now distinguishes claim coverage from semantic support explicitly. The mechanical gates were retained, not relaxed to make recorded outputs pass.
Golden tests freeze this reviewed LLM boundary, not a claim of byte-identical legacy HTTP responses. Full endpoint parity belongs to the application port.
SPDX-License-Identifier: EUPL-1.2
SPDX-FileCopyrightText: 2026 CE-RISE consortium
The response, not the request, says which backend answered
- Status: accepted
- Date: 2026-09-21
- Sprint: 3
Context
The workbench can be switched between two backends. The switch is a preference
held in the browser and sent as X-Backend-Mode on every request.
A preference is not a fact. The backend resolves the mode against what this deployment actually built: an unrecognised value falls back to the default, and so does a mode whose bundle was never constructed. Both fallbacks are deliberate — a typo in a header should not be an outage — and both mean the served backend can differ from the requested one.
Sprint 1 set X-Backend-Mode-Used in the Search handler alone. That was enough to
make Search honest and left every other window free to lie, including the 422 path,
where the handler never runs at all and is therefore exactly where a badge is most
likely to go stale.
Decision
The response header is the single source of truth about which backend answered, and it is set by middleware on every response.
Three consequences, each load-bearing:
-
ModeMiddlewareresolves the mode once per request, stashes the resolution onrequest.state, and stampsX-Backend-Mode-Usedon the way out. Theget_bundledependency reads that stash rather than re-resolving, so the handler and the header cannot disagree. A test asserts the body’smodefield equals the header. -
The header is set for every status, including the 422 a mode returns when it cannot serve a feature. An honest decline still has to say who declined.
-
The frontend badge renders the header and never the preference. When they differ it turns amber and says “fell back”, because a silently downgraded backend produces answers of a different rigour under an unchanged interface.
Two companion headers carry the rest of the resolution: X-Backend-Mode-Source
(“header” or “default”) and X-Backend-Mode-Warning, set only when a requested
mode was refused. All three are in the CORS expose_headers list; a browser cannot
read a response header that is not.
Consequences
The Settings panel offers only modes the backend reports as built — mode_allowed
comes from the bundle registry, not from configuration — so the UI cannot present a
choice that would immediately fall back. The fallback path still exists and is still
tested, because a stale browser tab can outlive a redeployment.
Cost: every response carries roughly sixty bytes it did not before, and one middleware frame runs per request. Both are negligible against a badge that can misreport which backend produced a number a reviewer is reading.
SPDX-License-Identifier: EUPL-1.2
SPDX-FileCopyrightText: 2026 CE-RISE consortium
A mode adds substrates; it never exchanges them
- Status: accepted
- Date: 2026-09-21
- Sprint: 3 (the rule dates from Sprint 2; this is its general form)
- Refines: ADR 0002, Mode as adapter swap
Context
ADR 0002 is titled “mode as adapter swap”, and the word swap turned out to be the problem. It describes the mechanism accurately — a mode is a different set of bound adapters — but reads as licence to exchange one adapter for another, which is the wrong instinct for a mode whose purpose is to mount more knowledge. This record does not overturn 0002; it constrains the direction of the swap.
ProviderBundle binds exactly one adapter per port. CE-RISE mode is built by
replace()-ing fields on the Normal bundle, which makes mounting new knowledge
look like a one-line assignment — and an assignment to a single slot is a swap.
This has now produced the same bug twice, in two different slots:
-
Sprint 2, the impact slot. CE-RISE mode assigned
PefdppImpactEngineintoimpact. The Carbon window immediately stopped working for the five products the graph has never heard of. Caught by a test asserting that a shared feature survives the switch. -
Sprint 3, the substrate slot. CE-RISE mode assigned
PefdppSubstrateRegistryintosubstrates. The CE-RISE Models window went from eighteen models to none the moment you switched to the more rigorous backend — knowledge present in the fast mode and absent from the careful one, which is exactly backwards. Caught while wiring the frontend, by asking both backends the same question.
Both were one word in a docstring away from being noticed: the design has always said CE-RISE adds. The word was load-bearing and the code did not enforce it.
Decision
A mode may only widen what is mounted. Where a port has one slot and two implementations are wanted, they are composed, not exchanged.
CompositeSubstrateRegistry holds an ordered tuple of member registries and
delegates:
mounted()concatenates, in mount order.facts_for()merges every member’s triples. A subject known to two substrates gets both views; suppressing one would silently pick a winner.coverage_report()concatenates per-substrate rows rather than summing them. ADR 0005 measures fire rate per substrate, and a pooled average would hide the very difference the instrument exists to show.- Capability methods the port does not declare —
route,query,run_question— are forwarded to the first member that implements one. Members are ordered most-specific-first, so the graph answers graph questions and the catalogue answers catalogue questions. The forwarding list is explicit rather than a blanket__getattr__, so a typo fails where it is written.
The invariant is asserted directly, not just per feature:
assert normal_substrates < ce_rise_substrates
A mode that removes knowledge fails the suite.
Consequences
facts_for now walks every member, so the cost of adding a substrate is linear in
mounted substrates rather than free. That is the correct shape: mounting more
knowledge should cost more, and the coverage report is there to say whether the
widening paid for itself.
The same reasoning applies to any port whose implementations are additive rather
than alternative. impact was fixed in Sprint 2 by keeping the CSV engine and
reaching the graph-solved assessment through the substrate instead; if a third
mounting ever wants two engines side by side, it composes.