InfraNotes Accounting Engine · v0
Welcome
Select a document from the sidebar to read it.
Smart Engine — Frontend Explanation Contract (Release 2)
- Status: Frozen for Release 2 accounting workspace MVP
- Date: 2026-05-13
- Audience:
infranotes-frontconsumers of the accounting workspace (Accounting and Close > Ledger / Reconciliation / Period Close) - Related:
infranotes-accounting-engine/docs/adr/0001-workload-identity.md(auth contract); INFT-76
The Smart Engine never speaks to the frontend directly — it is a backend-only gRPC service. The accounting stack (infranotes-core-ledger, infranotes-recon, infranotes-accounting-reports) consumes the engine's outputs and re-shapes them into HTTP read models that the FE renders. This doc names the engine fields the FE should surface, what they mean, and which downstream service exposes them.
Why the FE cares about engine explanations
Per INFRANOTES_PRIMAVERA_SURPASS_ROADMAP.md §11 (infranotes-accounting-engine role) and the cross-cutting recommendation §4 "Explainability and Auditability", the engine's differentiator is making automated decisions visible to accountants:
- Why a posting was generated →
ClassifyResponse - Why a match succeeded or failed →
MatchResponse - Why an anomaly was flagged →
AnomalyResponse - Why a close is blocked →
CloseReadinessResponse
Each of those proto responses carries one human-readable explanation string plus structured sidecar fields the FE composes with.
Explanation contract per RPC
1. ClassifyResponse — produced by classification (e.g. expense → account code)
| Proto field | Type | Always present | FE usage |
|---|---|---|---|
status |
ClassificationStatus enum (CLASSIFIED, CLASSIFICATION_FAILED) |
yes | Pill colour: success / "manual review" |
account_code |
optional string | when classified | The account the engine chose |
dimensions |
map<string, string> |
when classified | Cost-centre / project / department dimensions; render as key/value chips |
matched_rule_id |
optional string (UUID) | when classified | Used for "view rule" deep link to rule governance UI |
matched_rule_version |
optional string | when classified | Show alongside rule name so reviewers see exactly what fired |
explanation |
string | always | The primary human-readable line under the chosen account |
confidence |
double 0.0..=1.0 |
always | Render as a percentage badge with severity colour: green ≥0.85, amber ≥0.5, red <0.5 |
confidence_method |
string (rule_priority, specificity, feedback_weighted, match_gap) |
always | Tooltip on the confidence badge so accountants understand the scoring method |
evaluation_trace[] |
array of EvaluationTrace (rule_id, rule_name, rule_version, matched, condition_results, evaluation_time_ms) |
always | "Why this rule, not the others" drill-down panel — show the matched rule first, then the runners-up |
total_evaluation_time_ms |
uint64 | always | Diagnostic field; show only in advanced/debug mode |
previous_account_code / previous_rule_id / previous_rule_version |
optional | only in batch dry-run with include_diff |
"This used to classify to X" indicator in batch review UI |
classification_changed |
bool | always (false in non-diff mode) | Highlight changed rows during batch re-classification reviews |
diff_computed |
bool | always (false in non-diff mode) | Distinguishes "no prior evaluation to compare" from "compared and unchanged" |
2. MatchResponse — produced by reconciliation matching
| Proto field | Type | Always present | FE usage |
|---|---|---|---|
matched_pairs[] |
array of MatchedPair (source_item_ids, target_item_ids, match_type, score, max_score, matched_rule_id) |
yes | Render as joined pairs in the auto-match queue with a score bar (score / max_score) |
unmatched_sources[] |
array of NearMiss (item_id, optional closest_match_id, failure_reason, optional amount_difference, optional date_difference_days) |
yes | Render in the "needs review" queue with the near-miss as a one-click resolve candidate |
unmatched_targets[] |
array of NearMiss |
yes | Same as unmatched sources, from the GL side |
explanation |
string | always | Header text above the matching results table (e.g. "Matched 12 of 15 bank lines using rule 'Vendor reference v2.1'") |
3. AnomalyResponse — produced by anomaly detection
| Proto field | Type | Always present | FE usage |
|---|---|---|---|
anomalies[] |
array of Anomaly (rule_id, rule_name, severity, anomaly_type, target_type, target_id, details, context) |
yes (may be empty) | One row per detected anomaly. severity drives the badge colour (INFO/WARNING/CRITICAL). context is a map of values that triggered the rule — render as key/value chips |
has_blocking_anomalies |
bool | always | Drives the "Period not ready to close" banner |
explanation |
string | always | Header text summarising the detection run (e.g. "3 anomalies detected; 1 critical") |
The Anomaly.details field is a free-form string from the rule's action template — render as the primary description. The Anomaly.context map is the "evidence" pane (vendor name, amount, period total, etc.).
4. CloseReadinessResponse — produced by close-readiness evaluation
| Proto field | Type | Always present | FE usage |
|---|---|---|---|
is_ready |
bool | always | The big traffic-light at the top of the period-close page |
readiness_score |
int32 (0–100) | always | Score badge / progress ring |
blocking_items[] |
array of BlockingItem (rule_id, rule_name, description, resolution_action) |
yes (may be empty) | The "what's blocking close" checklist; resolution_action is the call-to-action text on the row's button |
checklist_summary[] |
array of ChecklistItem (task_name, status, passed, optional failure_reason) |
yes | The full close-readiness checklist; mark passed items green, failed items red with failure_reason |
explanation |
string | always | Header summary above the checklist (e.g. "Ready to close — 12 of 12 tasks complete") |
What the FE does NOT consume
These fields exist on engine RPCs but are not part of the FE handoff:
FeedbackRequest/FeedbackResponse— internal to the engine; rule-tuning analytics surface, not user-facing.RuleVersionRequest/RuleVersionResponse— rule-governance UI concern, exposed via the rule-management surface (out of scope for the accounting workspace).ValidateRulesRequest/ValidateRulesResponse— rule-authoring tool concern.RuleStatisticsRequest/RuleStatisticsResponse— rule-tuning dashboard concern.
Which downstream service exposes each RPC's output
The FE does not call the engine directly. The mapping:
| Engine RPC | Consumed by | FE surface area |
|---|---|---|
ClassifyTransaction / ClassifyBatch |
infranotes-core-ledger (journal-posting pipeline) |
Accounting > Ledger > Journal Entries (classification panel on each line) |
MatchReconciliation |
infranotes-recon (auto-match workbench) |
Accounting > Reconciliation > Auto-Match / Match Review |
DetectAnomalies |
infranotes-core-ledger (anomaly flags on journal posting), infranotes-recon (close anomalies) |
Anomaly badges on journal lines; "Open anomalies" panel on the close-readiness page |
EvaluateCloseReadiness |
infranotes-recon (close cockpit) |
Accounting > Period Close > Readiness |
RecordAnomalyDismissal |
infranotes-recon (user clicks "dismiss" on a flagged anomaly) |
Anomaly review modal |
RecordFeedback |
Internal — admin/tuning surfaces only |
Each downstream service has its own read-model shape; the engine's explanation fields propagate through into those HTTP responses verbatim. The FE consumer should look at the downstream service's API contract (infranotes-core-ledger/docs/openapi/core-ledger-api.yaml, infranotes-recon/docs/openapi/recon-service-api.yaml) for the actual JSON field paths the FE binds against.
Internationalisation note
All explanation strings the engine emits are currently English-only. Per the platform localisation roadmap, country-pack work owns translating these in Release 4. For Release 2, the FE may display them as-is or wrap them in <EnglishOnly> markers per the front-end i18n convention. The structured sidecar fields (rule_name, severity, etc.) are programmatic identifiers and never translated.
Versioning
The explanation text is not a stable contract — wording may change without a proto bump. The FE must never === compare on it. The structured fields (enum values, field names, UUIDs) are the stable contract; any change there will be communicated via the engine's proto changelog and a coordinated downstream-service update.
Open follow-ups (handoff to FE lane)
- Empty-state copy for each RPC's response — what to show when classification status is
CLASSIFICATION_FAILED, whenmatched_pairsis empty, whenanomaliesis empty. Owned by Product Design. - Rule-detail deep links — the FE renders
matched_rule_idandAnomaly.rule_idas link-outs; the target rule-governance UI isINFT-76Phase 2 scope (rule authoring/testing surface), not built yet. Stub as a tooltip until then. - Confidence threshold UI — the engine has a
LOW_CONFIDENCE_THRESHOLDenv var; FE could surface a tenant-configurable threshold setting on the rule-tuning page once that surface exists.