Control tests
10 control objectives, run against this system. Most inspect the code and the artefacts. 2 exercise it: CO7 derives a cache key for the same transaction under 2 model ids and again under 2 prompt versions, asserting each pair differs, and CO9 drives applicants through the decision engine until one is declined, then reads back the explanation that applicant would receive.
1 fail, 2 partial, 7 pass. Last run 2026-09-07. These results are generated by scripts/control-tests.ts and read directly by this page, so this is the last run rather than a copied summary.
Scope: 12 areas of 22, of which 10 carry an objective
The library was scoped against a 22-area checklist for auditing an AI system. An area carries an objective only when all 3 of these hold.
- 1The duty applies to the deployer of this system. Cadence deploys 3 model attachments and trains no model of its own. A duty that falls on whoever built a model, or on a whole institution, is outside a self-assessment of one system.
- 2The evidence a test would need can be obtained here. The code, the traces, the configuration and the applicant journey can all be read. Procurement records, network traffic and a company incident log cannot, and a test with no evidence behind it returns an opinion.
- 3What counts as passing comes from a rule outside this system. Every objective names the source that sets its bar: an AI Act article, a BaFin expectation, the OWASP list. Where no outside source sets one, the bar has to be invented, and the test then measures the auditor's own opinion of what good looks like.
The third criterion predicted the results. The one pass mark set here rather than by somebody outside, the 95.0% accuracy threshold on the categoriser, is the threshold reading above its bar while the accuracy underneath it separates by applicant group, which is finding F8.
What each of the 12 areas in scope produced
What each of the 12 areas in scope produced
- CO12. AI inventory
- CO25. Regulatory classification
- CO36. Provider or deployer role
- CO47. Impact assessment
- CO108. Disclosure
- CO512. Performance against a threshold
- none13. Drift and re-evaluation CO5 names the ECB drift expectation in its source, which reads as coverage. Its test asks for a threshold, a named owner and a stated consequence, and stops there. A re-evaluation cadence and a watch on the input distribution are both unmeasured, and the learn pages say the online layer that would watch for drift is unbuilt.
- CO914. Explainability
- CO715. Model change management
- CO816. Agent entitlements
- none17. Prompt injection and adversarial testing CO8 cites OWASP LLM06, which is Excessive Agency, and tests the entitlements the agent holds. Prompt injection is LLM01 and a different control. No test puts adversarial input through the categoriser or the rationale drafter, so the system's behaviour under a hostile statement line is unknown.
- CO620. Human oversight
2 of the 12 met the criteria for an objective and received none. Drift and adversarial behaviour are both things a reviewer of an AI system looks for early, and the library is silent on each. Recorded as F12.
The 10 areas with no objective, grouped by why
The 10 areas with no objective, grouped by why
Scoped out by the risk map the library came from
- 18. Vendor AI risk
- 19. Verification of the AI claim
The risk map put contractual and vendor risk out of scope, so no objective could be written under it. F5 is the consequence: both providers run on a free tier with no contract behind either, and the finding has no objective in the library to file itself under.
No institution behind the system to hold the evidence
- 1. Governance framework and accountability
- 3. Shadow AI discovery
- 4. AI risk register
- 21. Incident and complaint handling
Each needs evidence an institution holds: an accountability map across an estate, procurement records, network egress, an enterprise risk register, an incident procedure with an owner and a deadline. A demonstrator holds none of them, and a test would report a fact about the scope.
Omitted rather than excluded
- 9. Data quality and provenance
- 10. Bias in data and outputs
Both risks were known when the library was written. The evaluation set takes its labels from the same generator that writes the transactions, and categorisation accuracy separates by applicant group. Both are recorded as findings, F7 and F8, and neither has an objective. Calling this a scoping decision would not survive the question.
A control exists and no objective requires it
- 11. Lawful basis and privacy
- 22. Capacity and availability
Consent is captured per bank, runs 180 days and is revocable. A daily budget of 300 model calls stops tracing first and leaves the application running. Both hold today because somebody built them, and the library would carry on unchanged if either were removed.
CO1Every AI attachment is inventoried, owned and classified
pass
Every AI attachment is inventoried, owned and classified
passSets the bar. IIA practitioner checklist
Method. Reconcile the declared inventory against the model call sites actually present in source, confirm every entry names an owner and a classification, and enumerate the providers and machine credentials the inventory should account for
- Inventory declares 3 attachments: categorise-transactions, build-rationale, judge-rationale
- Source carries 3 instrumented call sites in src/lib/llm.ts
- Undeclared in the inventory: none
- Declared but absent from source: none
- Entries without a named owner: 0
- Configured providers: openai/gpt-oss-120b, gemini-2.5-flash
- Machine credentials present: GROQ_API_KEY, GEMINI_API_KEY
Finding. The inventory now exists, names an owner and a classification for each attachment, and reconciles to the code. This test is the control rather than a one-off check: a fourth call site added without declaring it here fails CO1 instead of passing unnoticed. What it still does not do is reconcile against egress to model providers, which would catch a call made outside this module.
CO2Regulatory classification is assessed and documented
pass
Regulatory classification is assessed and documented
passSets the bar. AI Act Articles 6, 49
Method. Read the declared classification, confirm it reaches a conclusion with reasoning, an owner and a date, and re-perform the Annex III test on the lending use case against it
- Classification assessment: present, 4 reasoning steps, owner "Product owner, lending decisioning", assessed 2026-09-05
- Conclusion: High risk. Annex III point 5(b), with no derogation available.
- Use case is creditworthiness of natural persons, the one banking case named in Annex III 5(b)
- Decision logic is deterministic; the models label the inputs, draft the explanation and score it
- Article 6(3) derogation tested and rejected: the categoriser profiles a natural person's economic situation, and the final subparagraph makes a profiling system high risk in every case
- Re-assessment trigger recorded: Re-assess if the decision stops being deterministic, if a model output reaches the outcome with no deterministic step between, or if the product is offered to anyone other than a consumer borrower.
Finding. The classification is genuinely arguable, which is the reason it had to be written down rather than assumed. It concludes high risk under Annex III 5(b) and shows the step that decides it: the Article 6(3) derogation reads as available for a preparatory task, and the profiling subparagraph closes it. An assessor who disagrees now has a specific step to disagree with.
CO3The provider or deployer role is assessed and current
pass
The provider or deployer role is assessed and current
passSets the bar. AI Act Article 25
Method. Test whether fine-tuning, retrieval augmentation or prompt layering has occurred, then read the role assessment and confirm it carries a date and addresses each modification the code contains
- Prompt layering is present: few-shot exemplars in src/app/eval/page.tsx, src/lib/audit/findings.ts, src/lib/categoriser/prompt.ts, src/lib/llm.ts
- No fine-tuning and no retrieval augmentation detected
- Role assessment: present, assessed 2026-09-05, owner "Product owner, lending decisioning"
- Conclusion: Provider of the Cadence lending system. Deployer of the 2 third-party models it calls.
- Assessment addresses prompt layering: YES. Addresses fine-tuning: YES
- Re-assessment trigger recorded: Re-assess on any fine-tuning, on adding retrieval augmentation over customer data, on a change to the stated purpose of the system, and on adding a model provider.
Finding. Two roles run at once and the assessment separates them: provider of the Cadence system, deployer of the 2 models inside it. Prompt layering is named and dismissed as the deciding factor, and Article 25(1)(c) is what carries the conclusion. The consequence an institution would care about is recorded: Article 26 duties cannot be handed back to Groq or Google, so a conformity claim from either discharges nothing.
CO4High-risk deployments carry a fundamental rights impact assessment
pass
High-risk deployments carry a fundamental rights impact assessment
passSets the bar. AI Act Article 27
Method. Confirm the classification requires one, then reconcile the assessment against the 6 matters Article 27(1) lists and confirm each is answered
- CO2 concluded: High risk. Annex III point 5(b), with no derogation available. Article 27(1) names deployers of Annex III 5(b), so the assessment is owed.
- Assessment present: 6 sections, assessed 2026-09-05, owner "Product owner, lending decisioning"
- Article 27(1) matters covered: a, b, c, d, e, f
- Missing: none
- Risks of harm recorded under (d): 4
- Gaps the assessment declares about itself: 3
Finding. The assessment answers all 6 matters and declares 3 gaps in itself rather than reading as complete. The one worth carrying forward is under (d): categorisation accuracy is measured overall and never by applicant group, so the risk that error falls on irregular income and unusual salary vocabulary is reasoned rather than quantified. Article 27 asks about harm to specific groups, and an overall accuracy figure cannot answer that question.
CO5Model performance is monitored against a defined threshold with an owner and an action
partial
Model performance is monitored against a defined threshold with an owner and an action
partialSets the bar. BaFin lifecycle guidance; ECB drift expectation
Method. For every attachment in the inventory, require a defined threshold, a named owner of it, and a stated consequence on breach, then record how each consequence is applied
- Transaction categorisation: Overall accuracy against the 1,067-case evaluation set, threshold 95.0% overall. The measured figure is 96.1%. Owner: Product owner, lending decisioning. On breach: The deploy is held and the deterministic rules categoriser carries the traffic until accuracy is restored. Enforced by: Manual. The owner runs the evaluation from /eval before a deploy and reads the result, and the hold happens when the owner stops the push. A push to main deploys whatever the branch holds. Automating the check is open work.
- Decision rationale drafting: The 5 deterministic grounding checks, scored today on the deterministic template rather than on the drafted text (F10), threshold All 5 pass on every persona case Owner: Product owner, lending decisioning. On breach: The deterministic template is served in place of the drafted rationale. A person runs the check, reads the result and makes that change by hand. Enforced by: Manual. A person runs scripts/verify-rationale.ts from the command line and it computes a pass or fail. The repository carries no CI workflow and no git hooks, and the Vercel build command is the default next build, so a push to main deploys whatever the branch holds.
- Rationale judge: Groundedness the judge returns for a drafted rationale, on a 5-point scale, threshold 3 of 5 Owner: Quality owner, evaluation harness. On breach: The run is reported to the owner for review. The judge does not block a deploy, because its own agreement with human labels has never been measured. Enforced by: Reviewed by a person. It is the weakest of the 3: the other 2 at least have a script that computes a pass or fail, and this one rests on the owner's reading.
- Attachments without a threshold: 0. Without a named owner: 0. Without a breach action: 0
- Thresholds an automated gate applies: 0 of 3
- Deploy path: a push to main triggers a Vercel build that runs the default next build and publishes whatever the branch holds. The repository carries no CI workflow, no git hooks and no vercel.json, and package.json build runs next build alone, so a threshold reaches a deploy through a person who reads it and stops the push
- Categoriser accuracy scored group by group: 96.3% overall, 97.1% for everyone else, against a 95.0% gate
- Irregular income and a thin file: 32 of 1067 cases, measured 84.4%, 12.7% below the rest, and can still fall to 54.5% before the gate reacts
- Unusual statement vocabulary: 14 of 1067 cases, measured 64.3%, 32.8% below the rest, and can still fall to 0.9% before the gate reacts
- Detection limit of the gate: a group of 13 cases or fewer can reach zero accuracy without it being crossed
Finding. Partial, and the missing half is enforcement. Every attachment carries a level, an owner and a consequence, which is the part of the objective a register can satisfy by being written. Automatic enforcement stands at 0 of 3. A push to main deploys whatever the branch holds, so every consequence in this register reaches the system through a person who runs a check and acts on the result. scripts/verify-rationale.ts computes a pass or fail without an API key and comes closest to a gate, and a person runs it from the command line. The judge threshold is the weakest, applied by review with no script behind it, and that one is deliberate: the judge has never been measured against human labels, so a person keeps an unvalidated scorer out of the decision. An action a person has to remember is weaker than the objective describes, which is why this reads partial rather than pass. The categoriser threshold also fails at the job it is there for. Scored group by group, the model runs 12.7% and 32.8% below the rest of the population for the 2 groups a wrong label hurts most, while the overall figure reads 96.3% and the gate stays quiet. A threshold set on an average cannot react to a disparity underneath it. Recorded as F8.
CO6Human oversight is capable of changing the outcome
partial
Human oversight is capable of changing the outcome
partialSets the bar. AI Act Article 26; GDPR Article 22 after C-634/21
Method. Confirm an override path exists, that the reviewer receives reasoning rather than only a recommendation, and measure the override rate
- Override path exists and emits a score: src/lib/actions.ts
- GDPR Art. 22(3) human-review request is implemented: src/components/console/application-detail.tsx, src/lib/actions.ts
- Reviewer receives a grounded rationale and drill-to-source explainability, not a bare recommendation
- Override rate: 0 decisions recorded
Finding. The capability is real and now measurable, which it was not before this work. It is not yet evidenced: there is no recorded population of decisions to compute a rate over, and the quality of the reasoning the reviewer receives has never been validated against human judgement.
CO7Model changes reach change management
pass
Model changes reach change management
passSets the bar. BaFin lifecycle guidance
Method. Re-performance: derive a cache key for the same transaction under 2 model ids and assert they differ, then under 2 prompt versions and assert the same, so either change is a miss rather than a silent reuse. Then confirm the prompts carry an explicit version marker.
- Cache key under model-a: lohn/gehalt mustermann gmbh|3200.00|credit|model-a|cat-2026-09-05.1
- Cache key under model-b: lohn/gehalt mustermann gmbh|3200.00|credit|model-b|cat-2026-09-05.1
- A model change invalidates the cached label: YES
- Cache key under prompt-v1: lohn/gehalt mustermann gmbh|3200.00|credit|model-a|prompt-v1
- Cache key under prompt-v2: lohn/gehalt mustermann gmbh|3200.00|credit|model-a|prompt-v2
- A prompt change invalidates the cached label: YES
- Prompts carry an explicit version marker: YES, currently cat-2026-09-05.1
- Prompt version is recorded on every categorisation, rationale and judge trace
- Provider-side version updates are not notified and would not be detected
Finding. Both change paths now work and the test re-performs both. A label is stored against the model and the prompt that produced it, so a corrected exemplar reaches lines already labelled instead of stopping at new ones. What remains outside the control is the provider: a model updated silently behind a stable model id changes the answer without changing the key, and nothing here would detect it.
CO8Agent entitlements are managed as privileged access
fail
Agent entitlements are managed as privileged access
failSets the bar. BaFin access control expectation; OWASP LLM06
Method. Identify non-human identities holding provider credentials and test for vaulting, time-bounding, session logging and recertification
- Provider credentials held as long-lived environment variables: GROQ_API_KEY, GEMINI_API_KEY
- No vault, no expiry, no rotation record, no recertification
- No autonomous agent exists in this system, so no payment or transaction entitlement is held
See the evidence. Nothing to link. Provider credentials are server-side and are not exposed by the running system.
Finding. There is no agent here, so the excessive-agency risk does not arise. The machine identities that do exist hold unexpiring API keys in environment variables, which would not meet a privileged-access standard in production.
CO9Affected persons can obtain an explanation
pass
Affected persons can obtain an explanation
passSets the bar. AI Act Article 86
Method. Re-performance: produce a declined decision and read back the explanation the applicant would receive, then assess whether it is meaningful rather than merely present
- Declined case produced for Tomas Neuer
- Explanation returned (526 chars): "The application is declined because the affordability buffer check fails. Detected net monthly income is 2.347 €. After a living-cost allowance of 950 €, rent of 720 € and existing credit obligations of 0 €, 677 € remain..."
- States the outcome: YES
- Names the decisive factor: YES
- Carries the figures behind it: YES
Finding. This is the strongest control in the system. An explanation exists, is grounded in the decision package, states the outcome and the reason, and the console drills every figure to its source transactions. What has not been tested is whether a person who is not a loan officer would find it meaningful, which is the standard Article 86 actually sets.
CO10Disclosure is given where a person interacts with AI
pass
Disclosure is given where a person interacts with AI
passSets the bar. AI Act Article 50
Method. Inspect the applicant-facing journey for disclosure of automated processing and of AI involvement specifically
- Automated-decision disclosure present: src/components/apply/apply-wizard.tsx
- Explicit AI or model disclosure in the applicant journey: src/components/apply/apply-wizard.tsx
- The applicant is told a language model categorised the transactions and drafted the reasoning, and that the decision itself was made by deterministic rules
- GDPR Art. 22(3) human-review route is offered to the applicant on the same screen
Finding. Two disclosures answering to 2 different instruments are now both made on the decision screen. GDPR Article 22 is served by telling the applicant a decision was automated and offering review. Article 50 is served by naming what the model did and what it did not do, which matters here because the model touches the inputs and the explanation while the outcome is arithmetic. Neither disclosure has been tested for comprehension by anyone outside this build.