DealDocumentScreening/docs/SPIKE_PHASE0.md
2026-08-17 20:49:29 +03:00

103 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 0 Spike Report — Needle 2 prescreen & RustFS storage
> Date: 2026-08-16. Evidence from live testing in isolated environments
> (`/tmp/opencode/needle-spike`, ephemeral `rustfs/rustfs:latest` container).
> Gates the Phase 1 (extraction refactor) and Phase 2 (prescreen stage) plans.
## Spike 1: cactus-needle (Needle 2) for RU contract prescreen — **NOT VIABLE**
### What was verified
- `cactus-needle==2.0.5` is real (Cactus Compute, Apache-2.0, first release
2026-08-10). Installs cleanly on Python 3.13. The spec's claims hold:
14MB engine (fetched once from HF, cached offline), `needle.extract(text,
PydanticModel)`, calibrated `confidence` on responses, `.cact` LoRA
fine-tuning pipeline. Note: PyPI package name is **`cactus-needle`**
(`import needle`); bare `needle` on PyPI is an unrelated dead 2017 CSS
testing tool.
- Engine session RAM measured at 37.8MB (simple schema) to 119.7MB (larger
schema) — fine for a 256M container. Python deps pull the JAX/Flax/optax
stack (~200MB+ image impact) because fine-tuning support ships in the core
dependency list.
### Results (base weights, `buffer_size` default)
| Case | Result |
|---|---|
| EN invoice (README example) | Perfect extraction, `confidence=0.9484` |
| EN off-topic sentence | Correctly refused: empty call `[]`, `confidence=0.7427` |
| RU contract, 2.5K chars | **Total hallucination**: parties → "Supplier"/"Supplier", amount → 12.0 USD (real: 1 234 567,80 RUB), dates → 2024 defaults. `confidence=0.0` |
| RU contract, 43K chars | Same failure mode, different hallucinations. `confidence=0.0` |
| RU garbage text | **Not refused** — fabricated "Party A/Party B/150 USD". `confidence=0.0` |
### Verdict
The base model is **English-centric; it does not understand Russian**. The
calibrated confidence head behaves correctly (0.0 = "I can't read this"), so a
naive deployment would have been *safe* (100% → manual_review) but useless.
Fine-tuning cannot rescue this for v1:
1. Calibrated confidence is **disabled on tuned weights** (reports `None`) —
and the spec's entire routing design hinges on confidence gating.
2. The model failed even trivial verbatim copy tasks (could not copy
«ООО «Ромашка»» from adjacent text) — this is tokenizer/training coverage,
not prompt tuning.
3. Building a RU legal extraction dataset + LoRA pipeline is its own project.
### Plan adjustment (Phase 2)
- `worker-prescreen` ships with a **deterministic regex+pydantic extractor**
instead of Needle: RU/BY contract boilerplate is highly templated
(«договор поставки», «именуемое в дальнейшем», «сумма договора составляет
N рублей», «неустойка», «арбитражный суд», DD.MM.YYYY ranges). Zero marginal
cost, no hallucination, p95 ≈ ms. Absence of a match = `None`
`manual_review`, which matches the routing philosophy.
- `confidence_score` is redefined as a deterministic **field-coverage score**
(fraction of target fields extracted), not model calibration. Router
thresholds operate on coverage.
- `needle_client.py` / `model_weights` / `NEEDLE_*` env are dropped from the
plan; keep `prescreen_results.extractor_version` for future engine swaps.
- Optional fast-follow (not v1): small-LLM prescreen via the existing
`core/llm` port for fields regex missed.
- Revisit Needle only if/when a multilingual or RU-tuned base ships with
calibration intact.
## Spike 2: RustFS as MinIO alternative — **VIABLE, but stay on MinIO for now**
Tested with the project's own `MinioStorage` adapter (minio SDK) against
`rustfs/rustfs:latest`, single node, named volume.
| Check | Result |
|---|---|
| Health (`/minio/health/live`) | 200 — MinIO-compatible probe works |
| bucket create/exists, put/get/stat/delete | OK (incl. Cyrillic UTF-8 payloads) |
| `set/get/delete_bucket_lifecycle` (ILM config API) | OK — rule round-trips |
| **ILM expiration behavior** (`RUSTFS_ILM_DEBUG_DAY_SECS=2`, rule days=1) | **Object under `users/` purged in ~10s; control object outside prefix untouched** |
| Perf (4KB put+get ×20) | ~2.0ms/op |
| Docker ergonomics | Runs as UID 10001 — bind mounts must be chowned; named volumes are the easy path |
### Verdict
The critical blocker (152-ФЗ TTL purge via ILM) **works in practice** — the
"Under Testing" label upstream refers to CI gating, not absence. Remaining
reasons to keep MinIO as the default for production:
1. Project is weeks old (1.0.0-rc era); lifecycle, distributed mode, and KMS
are all still marked "Under Testing" upstream.
2. **SSE migration trap**: objects written by MinIO with SSE-S3/KMS/C are not
readable by RustFS — a later migration requires re-putting objects through
the app (we run SSE-S3=off in dev, on in prod per `S3_SERVER_SIDE_ENCRYPTION`).
3. No operational history; MinIO is battle-tested for this exact compose setup.
Because everything S3-shaped is already behind `core/s3/port.py`, switching
later is a compose + env change, not a code change. Action item: revisit when
RustFS marks Lifecycle + KMS GA (tracked in ARCHITECTURE notes, not code).
## Phase gate outcomes
- **Phase 1 (extraction refactor): GO** — unchanged.
- **Phase 2 (prescreen stage): GO with amendments** — regex extractor instead
of Needle; coverage-based confidence; no needle deps/env; conservative
routing (`PRESCREEN_AUTO_APPROVE=false` default) as already planned.
- **Storage: no change** — MinIO stays; RustFS is a credible fast-follow.