Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ GEMINI_API_KEY= # --model gemini (API-key path; the gemini CLI path ne
CONVERGENCE_HOST=127.0.0.1 # set to 0.0.0.0 only to expose beyond localhost (e.g. in Docker)
CONVERGENCE_PORT=8765

# --- web server limits (web/server.py) ---
CONVERGENCE_MAX_BODY_BYTES=16384 # largest accepted /api/chat request body
CONVERGENCE_LLM_TIMEOUT_S=30 # seconds before a chat backend call is abandoned

# --- web server auth (optional) ---
# If set, /api/chat requires a matching `X-API-Key` request header. Leave empty
# for localhost dev; set it for any exposed deployment, since /api/chat proxies
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ dist/
venv/
env/
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
htmlcov/

Expand Down
4 changes: 2 additions & 2 deletions ENGINEERING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ load-bearing nowhere a decision is made.
file — which a probabilistic classifier cannot give.
2. **The model sits at a narrow, grounded seam.** `conversation.py` receives only the
*structured findings*, never the raw corpus or the detection logic, via an injected
`complete()`. The model can rephrase or explain a verdict; it cannot move one. The
SDK is touched in exactly one file (`adapters/`).
`complete()`. The model can rephrase or explain a verdict; it cannot move one. Vendor
SDKs are imported only inside `convergence/adapters/`, one module per backend.
3. **Evals are first-class.** 100+ deterministic tests, a five-corpus discrimination
eval, doc-sync guards that fail on drift, and a scored report
(`demo.py --eval`). "It works" is replaced by numbers.
Expand Down
12 changes: 6 additions & 6 deletions FRAGMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ and where to extend it.
---

## L1 · borrow-authority / displace-accountability
`layers/pattern_detector.py`
`convergence/layers/pattern_detector.py`

Presenting a decision as the dictate of an outside authority ("the platform's
policy says…", "my accountant says…") so the speaker never owns it. A fragment
Expand All @@ -56,7 +56,7 @@ corpus seq-6 negative case.)
---

## L3 · claim contradicted (denial of agreement)
`layers/third_party.py`
`convergence/layers/third_party.py`

A denial fragment whose factual claim is tested against an external records set.
The denial is only the *trigger*; the contradiction is confirmed by a record, not
Expand All @@ -73,7 +73,7 @@ No matching record → nothing emitted. The engine never invents a contradiction
---

## L5 · register anomaly (formal-register fragments)
`layers/phrase_fragmentation.py`
`convergence/layers/phrase_fragmentation.py`

Not a tactic by itself — a *contextual* signal. These legalistic tokens raise a
message's "formal share"; a spike above the sender's own baseline flags a register
Expand All @@ -92,7 +92,7 @@ elevates a finding alone.
---

## L6 · cross-channel divergence (claim vs. admission)
`layers/cross_channel.py`
`convergence/layers/cross_channel.py`

Paired fragments: a favorable **claim** in one channel vs. the same sender's
contradicting **admission** in another. Aligned by sender *and* predicate.
Expand Down Expand Up @@ -139,11 +139,11 @@ same "the lawyer says" fragment, now read as the *justify* role.
Two layers carry no phrase fragments — their signal is **structure**, which is why
they are often the most decisive:

- **L2 · within-thread omission** (`layers/gap_detector.py`) — the building block is
- **L2 · within-thread omission** (`convergence/layers/gap_detector.py`) — the building block is
a *gap*: a message cut from inside one continuous thread (its shown neighbors on
both sides share its thread). You cannot see a splice by reading the splice; you
reconstruct the record and watch which interior pieces are missing.
- **L4 · domain convergence** (`layers/domain_convergence.py`) — the building block
- **L4 · domain convergence** (`convergence/layers/domain_convergence.py`) — the building block
is a shared **anchor** (a content word/bigram, ≥4 chars, non-stopword) recurring
across ≥2 independent domains. It also maintains an *anti-fragment* `STOPWORDS`
list — the connective tissue and generic verbs that must never count as anchors.
Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
A six-layer engine for analyzing a corpus of written communications. It does
**not** know or care whose messages it is fed. It was *built from* the structure
of message records — not *tailored to* any one conversation — so the **same
engine** runs on unrelated corpora with no code changes. (Fourteen data files
across the bundled corpora ship here — contractor, coparenting, channels,
grammar, and five dynamics variants; the only difference between runs is the
data in `data/`.)
engine** runs on unrelated corpora with no code changes. (Fourteen corpus files
ship here — contractor, coparenting, channels, grammar, and five dynamics
variants — alongside adversarial, holdout, and regression sets used by the
tests; the only difference between runs is the data in `data/`.)

**Who it's for:** anyone who needs to show *why* a finding holds with an
**auditable, deterministic** method instead of a black-box classifier —
Expand Down Expand Up @@ -194,6 +194,10 @@ convergence --corpus contractor --voice blanc # the Voice of Convergence
convergence --corpus grammar # coercion-grammar structural analysis
convergence --corpus dynamics # 5-type discrimination table
convergence --corpus db --db /path/to/your.db # run on your own SQLite export
convergence --corpus db --db x.db --db-table msgs --db-limit 500 # pick table, cap rows
convergence --summary # compact findings summary
convergence --summary --sender Morgan # ... only findings involving one sender
convergence --seq 12 # one message and the findings that touch it
convergence --eval # scored discriminator report
convergence --investigate # propose + verify new detectors
convergence --chat --voice blanc # conversational Blanc with Claude
Expand Down Expand Up @@ -271,7 +275,9 @@ convergence/ engine (corpus-agnostic; stdlib only in core)
conversation.py grounded Q&A seam (model injected)
layers/ one module per analytical layer (L1–L6)
adapters/ optional LLM adapters (Claude, OpenAI, Grok, Gemini, agy)
data/ 14 data files: contractor (sample_*), coparenting, channels (formal+casual), grammar, dynamics (dyn_*)
data/ 14 corpus files: contractor (sample_*), coparenting, channels (formal+casual), grammar, dynamics (dyn_*)
adv_*.json adversarial record-assembly cases (tests)
holdout/, regression/ held-out and regression dynamics corpora (tests)
tools/ generate_dynamics.py — seeded dynamics-corpus generator
tests/ 272 deterministic tests
web/ static frontend + local stdlib server
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ warn_unused_configs = true
# warn_return_any, strict_equality.

[[tool.mypy.overrides]]
module = ["openai.*", "anthropic.*", "google.*", "pexpect.*", "wexpect.*", "winpty.*"]
module = ["openai.*", "anthropic.*", "google.*", "winpty.*"]
ignore_missing_imports = true

[tool.coverage.run]
Expand Down
Loading