Somar agent architecture

How QEdit, Flex Entry, Research Agents, Statements, Reconciliation, and Evals Work

This page documents the current local implementation of the questionnaire-edit and flexible-entry systems. It emphasizes the deterministic Go code wrapped around faster models, especially Gemini Flash, so model judgment can be used without letting transcription mistakes, partial extractions, or malformed tool output reach production persistence.

Main batch editor internal/agents/qedit edits questionnaire JSON through guarded tools.
Evidence first internal/pipeline/qedit classifies, extracts, researches, and detects conflicts before editing.
Flexible cards internal/flexentry, flexbrain, and flexchat capture facts outside fixed questionnaire slots.
Regression harness internal/evalcore runs qedit, flex, and research suites with artifacts and warnings.

1. Executive Overview

QEdit is a schema-aware questionnaire editor. Flex Entry is the companion system for facts that should become flexible financial event cards instead of being forced into the questionnaire. Research agents are read-only document investigators. Statement extraction is the deterministic-heavy path for accounts and holdings.

Core rule: agents mutate only in-memory state. Pipelines decide what to persist, when to persist, and how to merge with existing production data.
1 Start with evidence Documents become statement extractions, research memos, typed claims, and conflict records.
2 Edit narrowly QEdit writes through one guarded JSON tool and stays inside schema and write-root boundaries.
3 Repair mechanically Go fills statement gaps, repairs positions, dedupes rows, and rejects unsafe IDs or values.
4 Route overflow Facts outside fixed questionnaire slots become flex cards and later MDA chat state.

Receives instructions, documents, notes, and a campaign. It loads the current questionnaire and schema, builds evidence, edits an in-memory JSON snapshot with update_json_value, validates each write, and persists once through Postgres in testing or Sherpas PATCH in production.

Receives overflow from QEdit and research memos, then creates FinancialEvent cards. Batch prefill keeps partial cards and marks them with incomplete_summary. Chat mode asks users for missing minimum fields or proposes estimates at finish time.

Reads VFS-mounted documents using line-bounded file tools. It returns Markdown memos with findings, sources, confidence, and flex candidates. It never writes questionnaire data and is frequently spawned in parallel.

2. Mental Model

The system uses models for semantic judgment and Go code for mechanical invariants. A model may decide which account a memo refers to, whether a fact belongs in the fixed questionnaire or flex, and which conflict needs a human. Go code handles path validation, schema sanitation, source chips, instrument IDs, portfolio rendering, statement-position integrity, conflict indexing, eval assertions, and persistence boundaries.

flowchart TD A[POST /qedit/jobs] --> B[Create ETL row] B --> C[Load docs and notes into VFS] C --> D[Classify documents] D --> E[Statement extraction] D --> F[Research memos and typed claims] E --> G[Conflicts and triage] F --> G G --> H[Deterministic statement prefill and evidence refs] H --> I[QEdit guarded in-memory edits] I --> J[Normalize, sanitize, validate] J --> K[Gap fill and statement integrity repair] K --> L[Flexbrain receives overflow] L --> M[PATCH Sherpas and complete ETL]

The same facts move through three states

The state is an in-memory questionnaire plus an ETL lifecycle row. The model maps evidence into schema fields and resolves ambiguous conflicts. The pipeline persists only after validation and merge.

Facts QEdit cannot represent are folded into an in-memory event list inside flexbrain, then persisted as etl_result.flex_events. Missing minimums become orange cards, not dropped facts.

Chat uses Scylla mda_state and append-only mda_messages. It asks users for missing fields, proposes estimates, summarizes sections, and routes form edits back through the QEdit port.

3. QEdit Request and Job Lifecycle

QEdit has two HTTP surfaces. The production surface is asynchronous and authenticated. The testing surface is gated by SOMAR_QEDIT_TESTING=on and returns fuller ETL snapshots for debugging.

POST /qedit/jobs creates a background ETL run and calls qeditpipeline.RunProduction. GET /qedit/jobs/{jobId}/status reads the ETL row and maps internal states to the poller contract: running, completed, or failed. Production reads the current questionnaire through the repo and persists through Sherpas campaign PATCH.

POST /qedit/testing/jobs is mounted only when SOMAR_QEDIT_TESTING=on. It calls qeditpipeline.Run and writes through qeditrepo.Repository.SaveQuestionnaireData. GET /qedit/testing/jobs/{jobId} returns a fuller ETL snapshot for local debugging.

Production modes

questionnaire_prefill General document/note prefill. It requires at least one document or note, runs evidence collection, applies deterministic statement prefill, calls QEdit if needed, repairs statement positions, and routes overflow into flexbrain.
questionnaire_scoped_patch One-row update mode, usually for a portfolio row from a single statement. It requires exactly one document plus writeTarget.arrayKey and rowId, resolves a concrete AllowedWriteRoot, and rejects out-of-root writes.

Job lifecycle

  1. Validate request body and mode. Bad UUIDs, unsupported buckets, missing documents, or invalid scoped targets fail before work starts.
  2. Create an ETL row with type questionnaire_edit, status running, target kind, campaign id, opportunity id, and target ref.
  3. Return {jobId} immediately.
  4. Run the pipeline in a detached goroutine with a 30 minute timeout and panic recovery.
  5. Complete the ETL row with etl.Result, or fail it with sanitized error status. Statement extraction results can be stored separately in extraction_result.

When the production pipeline fails after building a partial result, the production handler can still store that partial etl_result alongside the failed status. That preserves useful diagnostics and any structured evidence/result envelope available at the point of failure.

Agent core

qeditagent.NewAgent creates the Caravan agent named qedit. The default model in the current local code is google/gemini-3.5-flash, with structured final output validation, a high iteration cap, and conditional tools. Write tools use ctx.StateMu; read-only tools can run in parallel.

always: update_json_value conditional: resolve_instrument conditional: spawn_research_agent conditional: replace_string_in_questionnaire conditional: run_structured_extraction not exposed: apply_json_patch

4. Evidence Pipeline

The production pipeline no longer asks QEdit to read raw documents directly for prefill. It first converts documents into typed evidence: statement extractions, research memos, typed claims, and conflicts.

Evidence types

The evidence envelope in pipeline/qedit/evidence.go is the editor's prepared world model. It contains statement extractions, research memos, typed claims, and code-detected conflicts. Evidence.IsEmpty, Evidence.HasConflicts, and Evidence.TotalDocuments let the pipeline decide whether QEdit needs to run or whether deterministic prefill is enough.

StatementEvidence A document source plus statementholdings.StructuredStatementResult: accounts, positions, balances, holder names, account masks, and extracted source metadata.
DocumentMemo A research memo with source, findings, confidence, flex candidates, and parsed typed Claims.
Claim Structured facts, especially balances and goal amounts, used by conflict detection and QEdit magnitude guards.
Conflict A code-detected disagreement. Optional triage marks it noise, auto_resolve, needs_agent, or needs_advisor.

Classification

Document classification lives under internal/questionnaire/docs and is defensive by design. The classifier uses existing metadata and source mappings before calling a model. If it does call a model, it uses temperature zero, strict response format, enum validation, and fallback to OTHER. Low-confidence statement classification is downgraded so a weak model does not route a planning export into holdings extraction.

Credit-report body cues can override stale or misleading statement classification. Investment statement-looking text can be deterministically promoted after classification via looksLikeInvestmentStatement.

Collection dispatch

flowchart LR D[Classified document] --> S[Statement or holdings image] D --> T[Transcript] D --> N[Note] D --> X[Tax document] D --> C[Credit report] D --> O[Generic / other] S --> SE[Structured extraction] S --> SR[Statement research] T --> TR[Transcript research] N --> NR[Note research] X --> XR[Tax research] C --> CR[Credit research] O --> GR[Generic research] SE --> E[Evidence envelope] SR --> E TR --> E NR --> E XR --> E CR --> E GR --> E

Statements are special because they run two lanes: a structured holdings extraction for account data and a statement research memo for non-holdings planning facts printed on the same pages. Transcripts, notes, tax docs, credit reports, and generic files use specialized research objectives that all return the same memo shape.

CollectEvidence processes groups concurrently and returns partial evidence even when individual document workers fail. That keeps a single bad note or noisy extraction from throwing away usable evidence from other documents.

5. Statements, Holdings, and Deterministic Account Writing

Investment statements are the highest-risk area because they contain long arrays of accounts, positions, and lots. The current architecture deliberately removes as much transcription as possible from the editor model.

Structured extraction result

internal/agents/extractions/statementholdings/result.go defines the output schema. Numeric fields are pointers so unknown remains null rather than accidentally becoming zero. Positions explicitly say not to invent instrument UUIDs. The pipeline stamps source documents rather than trusting the model to produce provenance chips.

Extraction postprocessing

  1. appendExplicitZeroCashPositions preserves explicit zero-cash rows when documents state them.
  2. sanitizePositionInstrumentIDs removes invented or malformed instrument IDs.
  3. reconcileAccountAmounts reconciles account totals against extracted positions.
  4. resolveStatementInstruments maps known tickers/names to canonical instrument IDs.
  5. sanitizeAccountBankIDs and resolveAccountCustodians clean account identity metadata.
  6. normalizeAccounts stabilizes the final extracted account shape.

Retrying partial extraction

statement_extraction_retry.go detects suspicious extraction outputs. A statement with holdings signals should not produce zero accounts. An account with priced positions covering less than half the stated balance is likely under-transcribed. The pipeline retries once and keeps the better extraction based on priced position amount and account count.

Deterministic prefill

statement_prefill.go can build portfolios[] wrappers directly from statement evidence. It writes source-document chips, account masks, account metadata, and aggregated positions. For statement-only, conflict-free FH_PLUS or PORTFOLIO runs, Evidence.IsStatementOnly allows the pipeline to skip QEdit entirely.

Post-agent gap fill

Statement handling also runs after the editor. PrefillStatementPortfolioGaps appends statement accounts the model missed, while EnforceStatementIntegrity can append unmatched extracted accounts that still have positions. This gives the model room to make judgment calls without making account coverage dependent on perfect model recall.

Field-scoped conflict policy

noise False-positive conflicts do not block prefill. The account flows normally.
Balance auto_resolve When triage finds a clear winner, deterministic prefill uses that winner.
Balance needs_agent The wrapper and positions are still useful, so they flow; the ambiguous meta amount is omitted for QEdit judgment.
Position conflict Safe account metadata and non-conflicted data remain available. Positions still flow where the conflict does not invalidate them.
Account type ambiguity The whole account is quarantined because a wrong type changes downstream planning semantics.

Statement integrity repair

EnforceStatementIntegrity runs after the agent and normalization, before persistence. It matches wrappers to statement accounts by source-document chip, account identity, or type plus amount. If positions are under-counted, amountless, or sum-mismatched, it replaces positions from extraction using the same renderer as deterministic prefill. If an extracted account with positions has no wrapper, it appends the wrapper.

This is the core write-integrity refactor: keep the model's metadata decisions when useful, but make Go repair the long holdings arrays before anything reaches Sherpas.

6. Research Agents

Research agents are read-only subagents. They are used both inside QEdit and in dynamic miniagent workflows. Their job is targeted document discovery, not persistence.

QEdit research agent

The package internal/agents/qedit/research exposes research.Run. The tool payload is Args{Objective, PathHints}. Dependencies are a document VFS and FillTarget, which contains the editor instruction, questionnaire schema, and current questionnaire.

list_files Shows the VFS inventory with mime/name metadata. It intentionally does not include document bodies.
read_file Reads an exact path with a 1-based line range, capped at 500 lines per call.
read_files_batch Reads up to 10 exact path/range slices in parallel for side-by-side comparison.
search_files Regex-searches VFS files with context, capped at 50 files, 20 matches, 2 context lines, and 16KB output.

Research output contract

The QEdit researcher must produce Markdown sections: ### Findings, ### Sources, ### Confidence, and ### Flex Candidates. Findings should tag schema paths in backticks, cite VFS paths and line ranges, and avoid duplicating a fact in both Findings and Flex Candidates.

Spawn tool in QEdit

spawn_research_agent is conditionally registered. It is unavailable in scoped patch mode, unavailable when the preprocessed evidence context already exists, and unavailable when there are no documents. The parent QEdit agent never gets direct file read/search tools in normal prefill; it delegates narrow document work.

Evidence-collection research

In production prefill, research usually happens before the editor sees anything. CollectEvidence builds objectives for statement research, transcript research, notes, tax docs, credit reports, and generic docs. runResearchAgent retries once when a memo looks degenerate, for example too short, leaked tool strings, or missing Markdown headings.

Claims and flex candidates

Research memos can carry a trailing ### Claims JSON block. parseMemoClaims extracts those claims, while malformed or absent claims degrade to prose-only evidence. Goal claim amounts feed the QEdit goal magnitude guard. Flex candidate sections feed flexbrain through FlexCandidatesFromEvidence.

Dynamic miniagent research

internal/agents/flexoutput/dynamic_miniagents/research mirrors the same file tool shape for dynamic miniagents. It uses MemoryFileSystem, outputs Findings/Sources/Confidence/Ambiguity Notes, and is registered by internal/api/dynamic_miniagents/tools.go for manifests that require focused file investigation. Its search caps differ slightly from QEdit research: dynamic miniagent research searches up to 80 files rather than QEdit research's 50 file cap.

7. Write Integrity and Questionnaire Mutation

The main QEdit write tool is intentionally narrow. The model asks for a path and value; Go decides whether that write is legal, how to normalize it, what to sanitize, and whether it can be committed to in-memory state.

update_json_value flow

flowchart TD A[update_json_value] --> B[Lock StateMu] B --> C[Expand evidence references] C --> D[Canonicalize path and coerce values] D --> E[Apply semantic guards] E --> F[normalize.ApplyWithSchema] F --> G[sanitize.EditedPaths] G --> H[Business-rule sanitize] H --> I[ValidatePathScoped] I --> J[Record EditResult] J --> K[Commit to State.Questionnaire]

Important guards

$evidence expansion agents/qedit/evidence_refs.go and pipeline/qedit/evidence_refs.go let the model point at stable evidence IDs while Go copies exact account and position payloads.
Goal magnitude guard goal_magnitude_guard.go blocks clean 10x/100x slips such as copying $45,000 as $450,000.
Instrument ID guard instrument_id_guard.go allows only IDs already present, seeded from evidence, or returned by resolve_instrument.
Coverage validator coverage_guard.go rejects final outputs that skip evidence only because required fields are missing.
Scoped write root scoped.go and tools.go keep scoped patch jobs inside the resolved subtree.
Business rule sanitizer questionnaire/sanitize/businessrules.go removes duplicates introduced by the current edit while preserving legacy duplicates.

Path-scoped validation

The validator evaluates the document but only blocks on errors under the edited path. Pre-existing unrelated questionnaire problems do not prevent a valid local edit from being recorded. The edit result records capture status as full, partial, or none, and includes sanitized-field or business-rule details for downstream reporting.

Provenance

QEdit writes source attribution through citation IDs. Prompt context shows citation_id=... for documents. UpdateJSONValueArgs and ReplaceStringArgs accept citation_ids. ProjectProvenanceToQuestionnaire maps those citations into extraData.sourceDocuments where the schema supports chips, while loose fields retain provenance in State.Provenance and ETL references.

8. Conflicts, Reconciliation, and Resolution

Conflict detection is mostly deterministic. The model is used for triage and final judgment only when the code cannot safely decide.

Conflict sources

  • Statement vs statement: same account with different balances, account types, holder names, positions, quantities, or amounts.
  • Research claim vs statement: typed memo balance claims compared against statement accounts.
  • Account identity: masks win over names where available; same-source pairs are skipped to avoid treating two accounts in one consolidated PDF as one conflict.

False-positive controls

  • Claim matching emits at most one conflict per memo claim.
  • Ambiguous multi-match emits no conflict rather than quarantining likely-good data.
  • Approximate memo amounts get a wider tolerance.
  • Conflicts with both masks present but different masks are skipped as different accounts.

Triage

triageConflicts runs a single temperature-zero strict-JSON model call over all conflicts when TriageModel is configured. It can mark conflicts as noise, auto-resolve, needs agent, or needs advisor. Any triage failure degrades to original conflicts and does not fail the run.

Reconciliation in flex chat

Flex chat reconciles against state, not chat transcript memory. FinishConversation returns saved events as source of truth. ListFinancialEvents exposes current cards. Delete is deterministic. Edit and resolve signals inject internal instructions, then post-run validation checks whether the tool actually updated the card.

9. Flex Prefill: QEdit Overflow to Cards

Flex prefill is the batch path for facts that QEdit could not or should not place in the fixed questionnaire. It shares the flex event schema with chat but has a different gap policy: partial facts are kept as orange cards.

Overflow sources

QEdit edit outcomes AggregateOverflow and FormatOverflowSummary convert semantic rejections, no-matching-schema notes, business-rule removals, and failed edits into a structured <overflow> block.
Evidence coverage FlexCandidatesFromCoverage converts skipped, blocked, or unhandled coverage items with no questionnaire field into <qedit_not_represented>.
Research memos FlexCandidatesFromEvidence converts research ### Flex Candidates sections into <flex_investigation> for flexbrain.

Flex event schema

flexentry.FinancialEvent carries event_id, category_id, subcategory_id, data, title, description, chat_references, optional origin_event_id, timestamps, and incomplete_summary.

Embedded catalog

internal/flexentry/schema embeds category schema JSON. LoadSection, LoadSubcategory, and Categories provide deterministic access. FieldDefinition.Requirement marks minimum, default, or addition fields. Conditional minimums are skipped by hard validation because applicability is model-judged.

Batch gap policy

In batch prefill, the model cannot ask the user. The add_financial_event tool includes unsourced_minimums. If the model declares a minimum field unsourced, Go deletes any placeholder value for that key so MissingMinimums sees a real absence. If a minimum is neither filled nor declared, the tool soft-retries once. After that, the partial card is accepted and annotateIncomplete writes incomplete_summary.

10. Flex Chat and MDA

The MDA API adapts frontend chat requests onto flexchatpipeline.RunStream. It is stateful, streams SSE events, hydrates from prefill once, and persists shared campaign state after the stream drains.

Auth and storage

MDA is a machine-to-machine API like production QEdit. Routes are registered with X-Api-Key middleware backed by INTERNAL_API_KEY. Identity such as user type, client id, and advisor id arrives in the request body from the trusted upstream caller.

Conversation history and state are separate. mda_messages stores append-only message history per session. mda_state stores the shared campaign state keyed by campaign/questionnaire id. On the first chat turn, prepareState hydrates mda_state once from the latest succeeded QEdit prefill etl_result.flex_events. Before that hydration exists, the section events read endpoint can fall back to prefill cards from ETL so the UI can display prefilled flex events immediately.

API surface

POST /mda/campaigns/{campaignID}/sections/{sectionID}/chat Streams one chat turn through SSE. Requires session id, user type, questionnaire type, and client id.
GET /mda/sessions/{sessionID}/history Returns persisted append-only message history.
DELETE /mda/sessions/{sessionID}/history Clears MDA messages only; state and cards survive.
GET /mda/campaigns/{campaignID}/state Returns current shared MDA state.
GET /mda/campaigns/{campaignID}/sections/{sectionID}/events Returns flattened cards with completion metadata and missing fields.

Signals

pipeline/mda/run.go accepts pre-translated metadata.signal or the frontend-native convention where an internal turn has the signal as message text. It maps:

ACCEPT_SUGGESTIONS_CREATE_EVENT -> accept SKIP_SUGGESTION -> skip BUCKET_DELETED -> delete BUCKET_EDITED -> edit RESOLVE_INCOMPLETE_EVENT -> resolve FINISH_CONVERSATION -> finish

Deterministic signal behavior

acceptMaterializes the pending proposal into a saved card, removes the proposal, then runs the model to summarize and call generate_summary.
skipDiscards the pending proposal. Declined estimates are not kept as orange cards. The model runs only to finalize summary.
deleteRuns no model. Go removes the card, emits mda/financial_event_deleted, then emits terminal Complete.
editDoes not mutate card data directly. It injects edit context requiring update_financial_event, then retries if data is unchanged.
resolveInjects incomplete-card context and requires real values before updating. Signal turns skip stale-incomplete post-run retry.
finishUses the finish gate. If nothing changed since last summary, Go emits terminal Complete and skips the model.

Tools

Flex chat exposes card CRUD tools, schema tools, route-to behavior, resources, questionnaire edit/schema tools, questionnaire diff, portfolio positions, deferred ask_user_input, proposals, finish conversation, and summary generation. Tools validate event data before writing state. edit_questionnaire_field serializes within a turn so concurrent read-modify-write calls cannot clobber each other.

History and prompt robustness

HistoryTrimmingStore gives the model a filtered, compacted view while preserving full persisted transcript. Old large tool returns are compressed, noisy messages are filtered, and truncation avoids orphan tool results. On prompt-too-long 400 errors, the pipeline retries once with emergency history trimming and compact questionnaire context.

11. Failure Behavior

Failure behavior is intentionally layered. Some errors fail the job. Some degrade to partial evidence. Some become tool feedback to the model. Some are salvaged when all valid writes already happened.

Failure Behavior
Bad production request Return HTTP 400 before creating ETL row.
ETL row create failure Return HTTP 500; no detached job starts.
Pipeline panic Recovered in background goroutine and stored as failed ETL row.
Evidence worker fails for one document Collect partial evidence from the rest; do not throw away all work.
Conflict triage model fails Keep original conflicts; QEdit sees them as unresolved.
Research memo is degenerate Retry once. If still bad, record document-level error or continue with other evidence.
Statement extraction empty/partial despite holdings signals Retry once and keep the better extraction.
QEdit final structured output parse fails after writes salvageAgentRunResult can keep already-applied in-memory edits and produce fallback summary.
QEdit max iterations after writes Same salvage path when the max-iterations error matches the canonical pattern.
Flexbrain failure after QEdit succeeds Non-fatal; questionnaire edits can still persist and flex events are absent.
MDA prompt too long Retry once with emergency history trimming and compact questionnaire prompt.
MDA deferred tool pauses Suppress post-run retries and emit explicit Complete so frontend stops loading and renders the widget.
Status polling an internal error Handlers sanitize stored error to processing failed.

12. How Deterministic Code Reduces Wrong Behavior From Faster Models

The current production qedit variant uses Gemini Flash for QEdit, structured extraction, research, and classification. The code compensates by narrowing tasks, constraining outputs, and moving high-cardinality transcription into deterministic Go.

Patterns

Constrain the small decisionsClassifier, conflict triage, structured extraction, and QEdit final output use structured outputs. Cheap classifiers and triage run at temperature zero.
Degrade safelyUnknown classifier output becomes OTHER; low-confidence statement classification falls back to generic research instead of brittle extraction.
Compare typed dataparseMemoClaims and detectClaimConflicts compare structured claims rather than scraping arbitrary memo prose.
Retry obvious weak-model failuresEmpty or partial statement extraction and degenerate research memos get bounded retries before acceptance or degradation.
Remove transcription from the model{"$evidence":"statement-1-account-1"} and PrefillStatementPortfolios let Go copy long account and holding payloads.
Repair before persistenceEnforceStatementIntegrity catches under-transcribed positions, while path-scoped validation and sanitizers keep writes legal.
Block high-impact tool mistakesGoal magnitude, unknown instrument IDs, null scalar writes, and scoped-root checks fail early with model-actionable feedback.
Force coverage honestyvalidateCoverageDispositions and flex missing-minimum retries prevent models from skipping partial facts or forgetting gap declarations.

Current active model variants

QEdit productionGemini Flash for QEdit, structured extraction, researcher, and classifier; Sonnet for flex.
Flex baseline-sonnetSonnet for flexbrain.
Research productionGPT-5.4-mini for isolated research routing evals.

13. Evals and What They Test

somar/cmd/eval runs three suites: qedit, flex, and research. evalcore builds the case-by-variant matrix, runs repeats with timeouts and parallelism, writes artifacts, computes metrics, and fails the CLI when quality gates fail.

The suite is intentionally weighted toward QEdit because that is where production persistence, statement extraction, document routing, and write integrity meet. Research evals isolate the memo router. Flex currently has a focused overflow regression around complete and incomplete cards.

Runner artifacts

Suite Artifacts
QEdit questionnaire.final.json, patch.payload.json, evidence.json, optional edit-snapshots.json, result.json, spans.json.
Flex events.json, result.json, spans.json.
Research memos.json, result.json, spans.json.

Assertion families

Generic

JSONPath exists/absent/equals/length/sum/set/find, unchanged checks, file equality, metric upper bounds, and duplicate detection.

QEdit

Success, persisted, minimum edit count, tool present/absent, write-root-only, patch exists, plus domain metrics such as qedit ms, extraction ms, research ms, conflict count, update calls, and resolve calls.

Flex and research

Flex counts complete/incomplete cards and subcategory presence. Research checks memo count, finding/flex contents, flex non-empty, and schema-field tags.

QEdit production-safety cases

Case What it protects
fh-income-inheritance-rentalIncome chip routing, partial capture, rental-property auto-link, insurance partials, and reduced flex leakage.
fh-plus-400-holdings-statementLarge holdings scale, no per-holding resolver churn, source provenance, FH_PLUS portfolio destination.
fh-plus-duplicate-sourceSame account in statement and meeting summary should become one account, not duplicates.
fh-plus-existing-account-conflictExisting account update should preserve row identity and avoid duplicate append.
fh-plus-meeting-statement-conflictStatement exact balance should win over approximate meeting balance while meeting planning facts still apply.
fh-plus-multi-account-statementOne consolidated PDF should split IRA, 401k, joint brokerage, and cash sweep accounts correctly.
fh-plus-partial-dataCapture available account data without hallucinating unavailable details.
fh-plus-same-custodian-multidocSame custodian documents remain separate when account identities/types differ.
fh-plus-stale-newer-statementNewer statement balance and provenance win.
fh-plus-unresolved-instrumentsAccount balance is captured even when positions cannot resolve to canonical instruments.
fh-routing-investmentsFINANCIAL_HEALTH writes account-level balances to finances.investments[], not detailed portfolios.
mixed-doc-transcript-statementsMixed statements, meeting summary, and advisor note produce portfolios, goals, income, liabilities, and no duplicates.
trace-012f-credit-report-regressionCredit-report liabilities and primary-home facts from production trace.
trace-67s-kailey-roth-401kScoped PORTFOLIO patch: write root only, structured extraction, and Roth 401k fields/positions.
trace-emoney-steadle-sherpas-data-transfereMoney plus meeting summary: DOBs, zip, primary home, salary, investment accounts.
trace-emoney-wysni-sherpas-data-transfereMoney-only production regression with DOBs and FH investment accounts.
trace-shupe-gd-lot-collapseSchwab GD lots collapse into correct Roth and Traditional positions without duplicate ticker rows.
trace-timothy-weah-mixed-docsLarge mixed-doc FH_PLUS trace covering portfolios, crypto, salary, liabilities, risk, and children facts.

Flex eval case

mixed-complete-incomplete feeds overflow facts with no fixed questionnaire slot. It asserts at least four events, presence of relocation, sabbatical, and parent-care events, and an incomplete start-business card. Inheritance and rental income are intentionally not flex facts because QEdit routes them into questionnaire income.

Research eval cases

Case Routing being tested
income-routingRental, inheritance, salary, and related facts must be Findings tagged to income[], not Flex.
mixed-flex-routingSalary to Findings; relocation, sabbatical, elder care, and side business to Flex only.
multi-section-routingRental condo tags both income[] and finances.nonInvestments[], no Flex leakage.
full-questionnaire-routingBroad FINANCIAL_HEALTH note routes fixed-schema facts across personal data, goals, income, real estate, investments, liabilities, insurance, expenses, risk; sabbatical to Flex only.
full-questionnaire-routing-fh-plusSame broad coverage for FH_PLUS, with investments routed to portfolios[]; sabbatical remains Flex only.

Quality warnings

QEdit eval warnings are non-gating unless strict portfolio checks promote them. They include empty statement accounts, no conflicts in mixed monetary evidence, duplicate summary lines, placeholder values, unknown portfolio types, holder mapping issues, missing source documents, position/account sum mismatch, high unresolved positions, invalid goal subtypes, unnecessary FH instrument resolution, statement-only client identity inference, high tool churn, final output not JSON, and statement extraction with no questionnaire edits.

14. File Map

QEdit agent
somar/internal/agents/qedit/agent.go somar/internal/agents/qedit/deps.go somar/internal/agents/qedit/state.go somar/internal/agents/qedit/tools.go somar/internal/agents/qedit/context.go somar/internal/agents/qedit/prompt.go somar/internal/agents/qedit/scoped.go somar/internal/agents/qedit/spawn.go somar/internal/agents/qedit/structured_extract.go somar/internal/agents/qedit/coverage_guard.go somar/internal/agents/qedit/goal_magnitude_guard.go somar/internal/agents/qedit/instrument_id_guard.go
QEdit research
somar/internal/agents/qedit/research/agent.go somar/internal/agents/qedit/research/deps.go somar/internal/agents/qedit/research/state.go somar/internal/agents/qedit/research/tools.go somar/internal/agents/qedit/research/prompt.go somar/internal/agents/qedit/research/run.go somar/internal/agents/qedit/research/fill_target.go somar/internal/agents/qedit/research/types.go
QEdit pipeline
somar/internal/pipeline/qedit/run.go somar/internal/pipeline/qedit/production_run.go somar/internal/pipeline/qedit/evidence_collector.go somar/internal/pipeline/qedit/evidence_conflicts.go somar/internal/pipeline/qedit/conflict_triage.go somar/internal/pipeline/qedit/evidence_context.go somar/internal/pipeline/qedit/statement_prefill.go somar/internal/pipeline/qedit/statement_integrity.go somar/internal/pipeline/qedit/salvage.go somar/internal/pipeline/qedit/overflow.go somar/internal/pipeline/qedit/flex_candidates.go
Statement extraction
somar/internal/agents/extractions/statementholdings/result.go somar/internal/pipeline/extractions/profile_statement_holdings.go somar/internal/pipeline/extractions/profile_statement_accounts.go
Questionnaire mutation
somar/internal/questionnaire/normalize somar/internal/questionnaire/sanitize somar/internal/questionnaire/merge somar/internal/questionnaire/schema somar/internal/questionnaire/editresult somar/internal/validator
Flex prefill
somar/internal/flexentry somar/internal/agents/flexbrain somar/internal/pipeline/flexentry
Flex chat / MDA
somar/internal/agents/flexchat somar/internal/pipeline/flexchat somar/internal/pipeline/flexchat/prepare.go somar/internal/pipeline/mda somar/internal/api/mda
Stores and repos
somar/internal/stores/etl somar/internal/repos/qedit somar/internal/repos/mdaprefill somar/internal/repos/mdaresource somar/internal/repos/mdainstruments
Evals
somar/internal/evalcore somar/internal/evalsuite/qedit somar/internal/evalsuite/flex somar/internal/evalsuite/research somar/evals/qedit somar/evals/flex somar/evals/research
Docs referenced
somar/docs/qedit-agent.md somar/docs/roadmap-flex-entry-agent.md somar/docs/roadmap-mda-agent.md somar/docs/qedit-write-integrity-refactor-plan.md somar/docs/qedit-eval-remediation-plan.md

Operational commands

# Run standard tests
make test

# Run QEdit/Flex/Research evals
make eval SUITE=qedit
make eval SUITE=flex
make eval SUITE=research

# Local mock server
SOMAR_SCYLLA_ADAPTER=mock \
SOMAR_POSTGRES_ADAPTER=off \
SOMAR_SHERPAS_ADAPTER=mock \
SOMAR_FIRECRAWL_ADAPTER=off \
SOMAR_CLICKHOUSE_ADAPTER=off \
MOCK_FIXTURES_DIR=/workspace/somar/tests/fixtures/mock \
go run ./somar/cmd/agents