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

5.4 KiB
Raw Blame History

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 = Nonemanual_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.