Written for a reader who will test each number against the convention it is named for. Built from the model register, so it describes the engine that ships; the built-from stamp in this page's source is checked against the register in CI.
This is an analytics tool, not investment advice. It does not know your whole financial picture unless you enter it, it makes no suitability determination in the regulatory sense, and no recommendation it produces has a validated forward track record. Read it as a second opinion with its working shown.
Each line names the mechanism a reader can check. NOT YET is written where it is true.
| Standard | What the advisor does | Where to check |
|---|---|---|
| I.C Misrepresentation | Every published figure names its coverage and its basis. Unknown inputs are never substituted with a plausible number; a sector the vendor does not describe is reported as unclassified, never as a sector. | signal_engine.GUARDED_INPUTS; advisor_engine._sector_or_none; tests/invariants/test_advisor_absence_invariants.py, test_fund_lookthrough_invariants.py |
| III.C Suitability | A minimum profile set is scored before a plan is more than a draft. Below it, every action is marked review-only, loses its timing and names the missing fields. The set must also be CONFIRMED as current: only the client's explicit 'Confirm my profile is current' act - bound to the six values and their sources exactly as shown, and stored with them - records a confirmation; saving a form or retaking the questionnaire is an edit, never a confirmation. Constraints the client declared or answered (drawdown, exclusions, liquidity, objective - the objective in the client's own words from the questionnaire when not declared - and the allocation policy) narrow the plan; a proposal is never widened. The questionnaire's risk profile is the lower of risk tolerance and risk capacity, and when it sits below the stated objective the profile governs. ONE verdict governs every surface that tells a client to trade - the report, Today (its deploy-cash nudge and its policy cards included), the signal lists, the holdings column and the Tax checklist, the chat and its trade drafts, the market-discovery alerts, the Earnings Recap and Sector Lab: on a profile that is incomplete, not confirmed within the refresh cadence, or for a household or another member's book, every trade (buy, sell, trim, rotate, harvest) is shown for REVIEW with the reason and no pre-filled trade; a trade a declared constraint could not be checked against is a review too, never a pass. The drawdown limit is tested against the WORST severe case the engine modelled (the severe-recession row or the book's own crisis replay) and the basis is named with its population; once it is breached, or the policy says a class is over, any addition with exposure to it is held. Exclusions match the client's words against ticker, name, sector and industry on every buy surface and the names hidden are counted, once per name; a fund is matched on its own name and category, and its weight in an excluded sector is disclosed; a stock with no industry on file cannot be matched and is a review, as is a buy for an income or preservation objective whose beta or dividend is unknown. Near-term needs are compared with idle cash after the declared reserve, which is never offered for investment. A profile, reserve, exclusion list or saved questionnaire that cannot be read is stated as unreadable, never treated as empty or zero; review-only signals are not counted as actions. This is an analytics analogue of a determination, not a regulatory determination. | advisor_engine.MINIMUM_SUITABILITY_FIELDS, _suitability_completeness, _suitability_verdict, _objective_narrows, engine_signals, narrow_signals, _suitability_gate, _drawdown_test_case, _exclusion_hit, suitability_panel, load_investor_profile; portfolio_db.confirm_suitability_review; riskprofile.assess; assetmix.deployable_cash; tests/invariants/test_today_suitability.py, test_drawdown_gate_basis.py, test_exclusions_everywhere.py, test_advisor_policy_direction_invariants.py, test_advisor_suitability_completeness_invariants.py, test_advisor_suitability_invariants.py, test_kyc_stamp_is_a_confirmation.py, test_risk_profile_lower_of_two.py, test_suitability_review_fixes.py |
| III.C Suitability — allocation policy | A declared investment policy is the first lens and a breach is the first action. With no policy on file the questionnaire's mix is shown as a PROPOSAL awaiting acceptance; drift is never measured against it. A breach names EVERY class outside the band, not only the largest, and states the trade in DOLLARS: the over-weighted class and the under-weighted one are a single trade, and naming one side leaves half an instruction. Every class moves to the target the client declared, so the legs balance. The funding split is reported because it decides the cost - money moved out of cash is not a disposition and realises nothing, while selling equity or bonds realises a gain or loss in a non-registered account. The TAX ITSELF IS NOT ESTIMATED: it depends on which lots in which accounts are sold, which this path is not given, so an unknown stays unknown (I.C). Nothing is executed - the report proposes and the client acts. Rebalancing to a policy the client declared is arithmetic, so no claim is made that it improves a return or reduces risk by any figure; none has been validated (V.A). A saved policy carrying more than 5 points more equity than the client's own risk profile (questionnaire or declared tolerance) is STATED as a recorded departure on every surface that carries the policy; the policy is never changed for the client, and when the updated questionnaire method proposes a different mix the report says so. | advisor_engine._ips_drift, _policy_rebalance_trade, _enforce_policy_first, _policy_statements, _proposed_policy_from_questionnaire; riskprofile.policy_departures; tests/invariants/test_advisor_ips_drift_invariants.py, test_policy_rebalance_invariants.py, test_goal_feasibility_invariants.py, test_policy_departure.py |
| V.A Diligence and Reasonable Basis | NOT YET. No recommendation, score or diversifier pick has a graded out-of-sample record. Picks are logged with entry price and distance from the 52-week high; the grader abstains until a cohort matures. Nothing may describe the output as validated, backtested or predictive until it does. | advisor_picks table; scripts/advisor_pick_track_record.py; scripts/deep_score_track_record.py (UNCERTIFIED) |
| V.A — goal feasibility | A goal is measured against the expected return of the policy mix on FP Canada's 2026 Projection Assumption Guidelines, net of the book's fund MER and any declared advisory fee when every fund's MER is on file and labelled gross (with the reason) when not, with a seeded probability of success; a shortfall is answered with the client's levers — contribution, horizon, target — never with more risk. | retirement_sim.PAG_REAL_RETURN_PCT, PAG_INFLATION_PCT, return_basis, goal_success_probability; advisor_engine.book_fund_costs, _goal_feasibility; tests/invariants/test_goal_feasibility_invariants.py, test_plan_net_of_fees.py |
| V.B Communication with Clients | Fact and opinion are separated: engine figures are reconciled against the written narrative and a mismatch is disclosed; a constraint inferred from the questionnaire is labelled as such beside one the client declared; the AI-unavailable state is a visible banner, never a silent stub. | advisor_engine._validate_narrative, _figure_mismatches, investor_profile._provenance; tests/invariants/test_advisor_narrative_validation_invariants.py, test_risk_questionnaire_reaches_the_advisor.py |
| V.B — what a recommendation rests on | Two dependences are disclosed by the ENGINE, after the model has written, so the model whose output they qualify cannot omit them. (1) Where a recommended single name owes more of its Deep Score to the Growth pillar than an evenly-scoring name would - above the pillar's own weight share of 25% - that share is stated in the recommendation itself, together with the finding that long-term earnings growth has shown no persistence beyond chance (Chan, Karceski & Lakonishok 2003) and that the fastest-growing names have historically underperformed over five years (Lakonishok, Shleifer & Vishny 1994). The text says screen, not forecast: no forward claim is made, because none has been validated (V.A above). (2) The share of the portfolio held as individual securities rather than funds is stated against the 20% default budget, with its basis (Bessembinder 2018; Statman 1987; Domian, Louton & Racine 2007). NEITHER CHANGES A PICK, A RANK OR A SIZE, and the single-name budget is disclosed, not enforced - no trade is blocked and no plan rejected for exceeding it. | advisor_engine._apply_engine_disclosures, _disclose_growth_pillar_dependence, _CANDIDATE_FIELDS; intel_agent._growth_pillar_share; strategy_report._single_name_note, _SINGLE_NAME_BUDGET_PCT; tests/invariants/test_growth_and_concentration_disclosure.py |
| V.A - what a diversification claim rests on | A single name is RESEARCH and carries no position size. Each one states the basis for calling it a diversifier: the screen selects candidates OUTSIDE the client's overweight sectors, which is a real basis and a weak one - two different sectors can move together closely. Where enough price history exists the report adds the MEASURED change in the portfolio's effective number of independent bets, computed through the same engine the Risk X-Ray uses; measured 2026-09-24, only 4 of 10 live candidates carry enough history, and the other 6 say NOT MEASURED rather than staying silent. A candidate that measurably does NOT diversify is disclosed as such beside the recommendation. The screen is also subject to a SUSPENSION rule: if graded cohorts persistently underperform it stops recommending single names entirely and reverts to allocation-level advice. That rule cannot fire yet - no cohort has been graded - and it reports that state explicitly rather than an all-clear. | advisor_engine._disclose_diversification_basis, _diversification_delta, _sleeve_suspension, _apply_sleeve_suspension; risk_data.build_returns_panel; risk_concentration.compute_concentration; tests/invariants/test_diversification_basis.py, test_sleeve_suspension.py |
| V.C Record Retention | Every report is stored with its prompt version and model id; every scan date carries the git SHA that produced it, so a changed number can be attributed to the code or to the data. | advisor_engine._prompt_version; scanner.record_scan_provenance; scripts/scan_lineage.py |
| VI.A Disclosure of Conflicts | The platform holds no positions, earns no commission on any action and is paid only by subscription; AI generation cost is shown to the user before each run. | web/components/instrument/ai/DisclosureFooter.tsx; the access/spend confirmation flow |
monitoring: NOT MONITORED · metric: forward outcome of TRIM/SELL signals
validated by: tests/invariants/test_advisor_signal_invariants.py + test_advisor_absence_invariants.py — soft-reason tax guard, verdict never silence, tax always stated, one decider, unknown is never a number
monitoring: LOGGING — picks recorded since 2026-09-10, none matured; not yet graded · metric: forward return of diversifier picks vs SPY, by cohort · log: advisor_picks
validated by: tests/invariants/test_advisor_*.py, test_fund_lookthrough_invariants.py, test_stress_replay_invariants.py, test_risk_questionnaire_reaches_the_advisor.py, test_growth_and_concentration_disclosure.py — absence never a number, registered-first allocation, AI figures reconciled to the engine, fund fold-through on both spellings of absence, and the two engine-enforced disclosures: the threshold is the pillar's own weight share, an unmeasured share is silence and never 0%, the candidate whitelist carries the measurement through to the report, and no disclosure makes a forward claim the score has not been validated for (CFA V.A), each proven red by 16 entries in tests/parity/test_gate_injections.py
CompoundWise internal · not investment advice · generated from data/model_register.json (register 2.0.0, 2026-09-23).