# Oshun v1 — Audit pass 7: domain-correctness sweep

Date: 2026-05-28 Owner: platform team Previous passes:
[v2](./2026-05-28-v2-gap-audit.md) · [v3](./2026-05-28-v3-gap-audit.md) ·
[v4](./2026-05-28-v4-gap-audit.md) · [v5](./2026-05-28-v5-gap-audit.md) ·
[v6](./2026-05-28-v6-gap-audit.md)

## Why this pass exists

Passes 1-6 were **structural** gap-finders: "is there a guard / test / secret /
RLS policy here?" — answerable by pattern-matching against known anti-patterns.
The pass-6 doc claimed the remaining class (domain-business-logic correctness)
"needs product-owner driven review." That conflated two things:

- **Spec-conformance** — _does the code compute the right answer / enforce the
  right transitions?_ This **is** machine-checkable, by an agent that
  reconstructs each domain's invariants and checks the implementation against a
  ground truth (a textbook formula, a known-correct value, a structural
  invariant). That's this pass.
- **Spec-correctness** — _is the formula/policy itself right for our
  business/jurisdiction?_ This needs domain authority — the true residue.

Three read-only domain-correctness agents swept the highest-blast-radius
surfaces (money/ledger, moderation/safety, RAG/retrieval), each reconstructing
invariants and hunting for places the implementation produces a wrong answer or
allows an illegal transition. **35 confirmed divergences**, each with a concrete
wrong value or illegal sequence.

## In-session fixes (with oracle/property tests)

| Finding                                                                                                                    | Severity | Fix                                                                                                                                                                                    | Test                                                                                                |
| -------------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| **Money-5** promotion `free_period` produced negative `finalAmount` (platform pays the customer)                           | HIGH     | `svc-payment-orchestrator/.../promotion-service.ts` — clamp `free_period` discount to the charge; defense-in-depth clamp at `applyPromotion` so finalAmount ≥ 0 for all discount types | promotion-service.test.ts: rewrote the test that asserted the buggy 20000, +2 clamp tests (81 pass) |
| **Mod-1** escalated items were a permanent dead-end — escalation target could never resolve them                           | HIGH     | `aphrodite/admin/.../moderation/service.ts` — add `'escalated'` to the actionable-state guard                                                                                          | service.test.ts: 3 tests (escalate→approve, escalate→ban, terminal still rejects)                   |
| **Mod-7** chat link allowlist used substring `includes` — `evil-twitch.tv.phishing.com` passed an allowlist of `twitch.tv` | MEDIUM   | `aphrodite/chat/.../content-filter.ts` — parse host, exact-or-label-suffix match (same shape as Reg-1 OAuth fix)                                                                       | content-filter.test.ts: 5 tests incl. userinfo-spoof + look-alike (5 pass)                          |

## Verified NOT a bug (adversarial verification caught an over-claim)

- **Money-7 (TWR denominator)** — the agent flagged
  `(V_t−V_{t−1}−CF)/(V_{t−1}+CF)` as wrong vs `…/V_{t−1}`. But the code
  implements the **beginning-of-period cash-flow** convention (its comment says
  so), and the agent's "correct" value assumes **end-of-period** flow. Both are
  valid TWR conventions; which is right depends on _when_ the flow landed.
  Changing the divisor would swap one valid convention for another and risk
  regressions. **Left as-is**; filed only as a doc/clarity item (the convention
  should be documented + caller-selectable).

## Money / ledger findings (libs/lakshmi, lilith payment services)

| #    | Sev      | file:line                                                       | Invariant violated                                                                                                                       |
| ---- | -------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| M-1  | CRITICAL | `svc-creator-royalty/src/app.ts:167-190,560-566`                | platform fee + processing fee silently destroyed (recipients get 9701.25 of 10000); `totalDistributed += sale.amount` overstates payouts |
| M-2  | CRITICAL | `svc-micro-transaction/src/micro.ts:711-719,599-609`            | off-chain channel transfer credited at transfer AND re-credited at settlement → receiver nets +2A                                        |
| M-3  | CRITICAL | `svc-micro-transaction/src/micro.ts:1053-1140`                  | refund cap checks a single refund, not cumulative; `refundedAmount` overwrite not accumulate → double full-refund                        |
| M-4  | HIGH     | `lakshmi/retirement/src/rmd-calculator.ts:338-357`              | RMD-required at 70-71 but no factor <72 → falls back to age-120 factor 2.0 → distributes 50% of balance vs ~3.8%                         |
| M-5  | HIGH     | `svc-payment-orchestrator/.../promotion-service.ts:452-454`     | **FIXED** — free_period negative finalAmount                                                                                             |
| M-6  | HIGH     | `lakshmi/transactions/src/analysis/splits.ts:123-155`           | mixed fixed+pct+remainder: pct part scaled by sum-of-pct only → "50%" part gets 80% of base, remainder gets 0                            |
| M-7  | —        | `lakshmi/investments/src/performance/returns.ts:119-132`        | **NOT A BUG** — valid begin-of-period TWR convention (see above)                                                                         |
| M-8  | MEDIUM   | `svc-fiat-ramp/src/fiat-ramp.ts:1280-1309`                      | off-ramp quote recomputes fee breakdown on NET payout instead of GROSS → displayed fee ≠ deducted fee, tier thresholds flip              |
| M-9  | MEDIUM   | `svc-payment-orchestrator/src/stripe/refund-service.ts:288-322` | createRefund skips eligibility/maxRefundAmount; `!input.amount` conflates amount=0 with omitted → 0-amount refund cancels subscription   |
| M-10 | MEDIUM   | `svc-creator-royalty/src/app.ts:495-584`                        | sale processing non-idempotent — duplicate saleId re-pays recipients                                                                     |

Cross-cutting: royalty + fiat-ramp + promotion operate on JS floats with
`.toFixed(6)` rather than integer minor units — cent drift accumulates. Surfaces
verified **correct**: lakshmi debt (avalanche/mortgage amortization), tax
brackets, MACRS depreciation, investment rebalancing, settlement orchestration,
aphrodite tip split.

## Moderation / safety findings (aphrodite, kuanyin, themis)

| #    | Sev    | file:line                                                            | Invariant violated                                                                                                                       |
| ---- | ------ | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| MD-1 | HIGH   | `aphrodite/admin/.../moderation/service.ts:340-346`                  | **FIXED** — escalated dead-end                                                                                                           |
| MD-2 | HIGH   | `aphrodite/admin/.../moderation/service.ts:329-388`                  | no separation-of-duty — same reviewer who flagged/escalated can self-approve; `dismiss`→approved clears the flag                         |
| MD-3 | MEDIUM | `aphrodite/admin/.../moderation/service.ts:359-384`                  | `request_edit` (stays pending) still writes reviewedAt/reviewedBy → skews review-time metrics                                            |
| MD-4 | LOW    | `aphrodite/admin/.../moderation/service.ts:383`                      | `escalateTo` reassigns item on non-escalate actions                                                                                      |
| MD-5 | HIGH   | `kuanyin/performer-protection/src/boundary-enforcement.ts:3560-3579` | progressive enforcement recomputed from ratio each call → non-monotonic; a critical offender gets _downgraded_ by a later mild violation |
| MD-6 | LOW    | `kuanyin/performer-protection/src/boundary-enforcement.ts:3598-3599` | progressive recordId reconstruction can collide across viewer/performer pairs; re-scan double-counts violations                          |
| MD-7 | MEDIUM | `aphrodite/chat/.../content-filter.ts:179-188`                       | **FIXED** — substring allowlist bypass                                                                                                   |
| MD-8 | LOW    | `aphrodite/chat/.../content-filter.ts:253-267`                       | duplicate-message check off-by-one vs configured max (debatable semantics)                                                               |

Surfaces verified **correct**: themis academic-integrity appeals (SoD properly
enforced), classifier policy thresholds, originality-shield fail-closed cascade,
kuanyin samma-vaca sequential gates.

## RAG / retrieval findings (iris/knowledge, sophia, conversation-rag, svc-ai)

| #    | Sev      | file:line                                                                                              | Correctness property violated                                                                                        |
| ---- | -------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| R-1  | HIGH     | `iris/knowledge/rag/src/response-generator.ts:469-584`                                                 | grounding uses word-overlap (Jaccard) as entailment proxy → negated/contradicted claims marked "grounded"            |
| R-2  | HIGH     | `iris/knowledge/rag/src/response-generator.ts:469` + `grounding/.../hallucination-detector.ts:813-819` | Jaccard penalizes long chunks → genuinely supported short claims marked ungrounded                                   |
| R-3  | CRITICAL | `iris/conversation-rag/src/source-tracker.ts:414-516` (real path)                                      | citations attached by source-TITLE keyword, ignoring content → `[n]` points to wrong source                          |
| R-4  | HIGH     | `iris/knowledge/grounding/src/source-citation.ts:386-415`                                              | cosine ≥0.9 ⇒ `direct` support; substring ⇒ strength 1.0 — paraphrase/contradiction labeled direct                   |
| R-6  | HIGH     | `iris/knowledge/rag/src/reranker.ts:147-151`                                                           | `assessQuality` re-sorts cross-encoder results by bag-of-words overlap, destroying the rerank                        |
| R-7  | MEDIUM   | `iris/conversation-rag/src/relevance-scorer.ts:86-89` (real)                                           | boosts applied AFTER sort, array never re-sorted                                                                     |
| R-8  | HIGH     | `iris/conversation-rag/src/relevance-scorer.ts:80-83` (real)                                           | softmax/minmax normalize THEN minScore filter → all chunks filtered (zero context)                                   |
| R-9  | CRITICAL | `iris/knowledge/retrieval/src/hybrid-retriever.ts:611-628`                                             | cross-encoder `scores[i]` indexed by unfiltered result index when a doc is missing → scores misaligned to wrong docs |
| R-10 | MEDIUM   | `iris/knowledge/rag/src/retriever.ts:351-518`                                                          | RRF dedups per-method not per-source-list → multi-query hits double-counted                                          |
| R-11 | CRITICAL | `iris/conversation-rag/src/rag-pipeline.ts:964-1018` (real default)                                    | `buildContextFromChunks` enforces NO token budget → context overflows model silently                                 |
| R-12 | HIGH     | `svc-ai/src/rag/pipeline.ts:377-408` (real)                                                            | citations built from ALL docs but context truncated → cites unseen sources; `documentsUsed` overcounts               |
| R-13 | MEDIUM   | `iris/knowledge/rag/src/context-builder.ts:417-440`                                                    | strategic_ordering + budget truncation drops 2nd-best before worst; position labels use pre-truncation length        |
| R-14 | HIGH     | `iris/knowledge/chunking/src/semantic-chunker.ts:544-551`                                              | overlap copies chunk PREFIX not boundary suffix → no real overlap + corrupt offsets                                  |
| R-15 | MEDIUM   | `svc-ai/src/rag/pipeline.ts:610-618`                                                                   | paragraph chunking no maxChunkSize → chunk can exceed embedding model max seq length                                 |
| R-16 | MEDIUM   | `iris/knowledge/retrieval/src/dense-retriever.ts:476`                                                  | HNSW early-stop compares against unsorted candidates tail → prunes search early, lowers recall                       |
| R-17 | LOW      | `svc-ai/src/rag/pipeline.ts:638-652`                                                                   | empty/whitespace chunks not filtered before indexing in the svc path                                                 |

Surfaces verified **correct**: temporal-relevance-scorer, freshness-tracker
staleness, retrieval-lib hybrid fusion (RRF/CombSUM/CombMNZ/Borda dedup by
documentId), BM25 IDF/TF.

## Tracking

CRITICALs filed as discrete tasks (#300+). HIGH/MEDIUM/LOW are tracked in the
tables above with exact file:line + the proposed oracle test from each agent;
they feed the per-domain owner backlog. The three in-session fixes ship with
this commit.

## Is the residue now truly human-only?

Yes. After this pass, what remains is genuine **spec-correctness**: is the RMD
table the right one for the current tax year, is the moderation escalation
policy the one legal wants, is the grounding entailment threshold calibrated for
our content. Those are product/legal/domain-owner sign-offs, plus curating the
labeled eval sets that turn R-1/R-2/R-3 grounding fixes into regression
fixtures. The machine-findable conformance bugs — wrong formulas, illegal
transitions, misaligned scores, missing budgets — are now enumerated.
