diff --git a/.env.example b/.env.example index 2fb57f8..ca082d4 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,12 @@ # PostgreSQL DATABASE_URL=postgresql+asyncpg://utopia:utopia@localhost:5432/utopia -DATABASE_URL_SYNC=postgresql+psycopg2://utopia:utopia@localhost:5432/utopia +DATABASE_URL_SYNC=postgresql+psycopg://utopia:utopia@localhost:5432/utopia # WHOOP (placeholders — requires developer account) WHOOP_CLIENT_ID= WHOOP_CLIENT_SECRET= WHOOP_REDIRECT_URI=http://localhost:8000/integrations/whoop/callback -# AI Providers (placeholders) +# AI providers (placeholders) ANTHROPIC_API_KEY= OPENAI_API_KEY= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8bca057 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,79 @@ +name: CI + +on: + push: + branches: + - main + - "cleanup/**" + pull_request: + +permissions: + contents: read + +jobs: + secrets: + name: Secret history scan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: gitleaks/gitleaks-action@v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITLEAKS_ENABLE_COMMENTS: "false" + GITLEAKS_ENABLE_UPLOAD_ARTIFACT: "false" + + test: + name: Python / migrations / tests + runs-on: ubuntu-latest + + services: + postgres: + image: pgvector/pgvector:pg16 + env: + POSTGRES_USER: utopia + POSTGRES_PASSWORD: utopia + POSTGRES_DB: utopia_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U utopia -d utopia_test" + --health-interval 5s + --health-timeout 3s + --health-retries 10 + + env: + DATABASE_URL: postgresql+asyncpg://utopia:utopia@localhost:5432/utopia_test + DATABASE_URL_SYNC: postgresql+psycopg://utopia:utopia@localhost:5432/utopia_test + ANTHROPIC_API_KEY: test + OPENAI_API_KEY: test + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v7 + with: + python-version: "3.12" + cache: pip + + - name: Install + run: python -m pip install -e ".[dev]" + + - name: Compile + run: python -m compileall -q src tests migrations + + - name: Ruff + run: ruff check --select E4,E7,E9,F src tests migrations/env.py migrations/versions/014_operator_scoped_embeddings.py + + - name: Apply migrations from zero + run: alembic upgrade head + + - name: Verify cleanup migration is reversible + run: | + alembic downgrade 013 + alembic upgrade head + + - name: Test + run: pytest -q diff --git a/.gitignore b/.gitignore index 87d1481..4b2347b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,14 @@ build/ .venv/ venv/ .env +.env.* +!.env.example *.db *.sqlite3 +*.log +*.dump +*.sql +*.parquet .ruff_cache/ .pytest_cache/ .mypy_cache/ diff --git a/README.md b/README.md index ba32de3..08dcc62 100644 --- a/README.md +++ b/README.md @@ -3,184 +3,183 @@ > **Historical codename: Utopia** > **Status: historical / inactive / preserved** -Neurocognitive System was an experimental, backend-first architecture for state-aware cognitive control. +Neurocognitive System is a historical experiment in **multimodal cognitive-state estimation, continuity, reasoning, and adaptive control**. -It was built around one assumption: +It began from one assumption: **the operator is not stable.** -Attention changes. Energy changes. Context disappears. Re-entry has a cost. Present-state narratives can be wrong. A system that assumes a permanently consistent operator will eventually recommend the wrong thing at the wrong depth. +Attention changes. Energy changes. Context disappears. Re-entry has a cost. Self-report is useful but imperfect. Physiological and behavioral signals are useful but imperfect. The intended architecture was therefore not a task manager or an AI that simply "knows" the user. -The experiment asked whether software could preserve continuity by maintaining explicit models of direction, evidence, state, blockers, policy, action, outcome, and calibration. - -Its central loop was: +It was a system for maintaining explicit, testable hypotheses about the operator's current state and deciding what depth of action was appropriate. ```text Direction - ↓ + ↓ +Sensing + ↓ +Observations + ↓ +Features + ↓ Evidence - ↓ -State + Blocker Estimate - ↓ + ↓ +Inference + ↓ Policy - ↓ + ↓ Action / Re-entry - ↓ -Trace - ↓ -Review / Calibration - ↺ + ↓ +Outcome + ↓ +Calibration + ↺ ``` -The project is no longer actively developed as a product. - -It remains public because several architectural ideas explored here continued to recur in later systems. - **The implementation is historical. The questions were not.** --- -## The problem - -Most productivity software assumes a relatively stable operator. - -If the correct task is visible, the user can execute it. - -If context is lost, they can reload it. - -If they stop moving, the problem is assumed to be motivation, planning, or discipline. +## What this repository contains -This system explored a different model. - -The failure may be one of continuity. - -The operator may still know what matters in the abstract while temporarily losing access to the context, state, energy, or cognitive depth required to act on it. - -That produces a different set of questions: - -- What direction actually matters right now? -- What evidence describes the operator's current condition? -- What state is the operator in? -- What is actually blocking motion? -- What action depth is appropriate for that state? -- What is the smallest correct next move? -- How can interrupted work be re-entered without reconstructing everything? -- What did reality reveal after the action? -- Was the system's previous estimate actually correct? - -The goal was not to maximize visible activity. +The historical implementation includes: -The goal was to preserve direction and choose the smallest defensible move given the available evidence. +- Python 3.12+ +- FastAPI +- PostgreSQL 16 + pgvector +- SQLAlchemy + Alembic +- 13 historical database migrations + 1 cleanup migration +- typed ORM and Pydantic models +- directional hierarchy: life arcs → seasons → missions → threads +- subjective, behavioral, contextual, and derived evidence +- WHOOP ingestion for sleep, recovery, cycles, workouts, and body measurements +- state estimation, blocker classification, and policy selection +- typed memory and semantic retrieval +- structured problem reasoning +- re-entry artifacts and execution traces +- review and calibration records +- model/retrieval audit data models +- materialized read models +- service, AI-module, and API tests + +The Python package remains named `utopia` so the historical implementation stays intact. --- -## Not a task manager - -Neurocognitive System was not designed as a conventional productivity application. +## Multimodal intent -It was not primarily: +WHOOP was the first implemented external sensor, not the intended endpoint. -- a to-do list -- a note-taking system -- a mood tracker -- a habit app -- a diary -- a generic second brain -- a chatbot over personal data +The longer-term design was sensor-agnostic. It was meant to accept additional evidence sources such as: -The architecture treated cognition and execution as a control problem. +- behavioral telemetry +- environment/context sensors +- additional wearables +- biomarkers +- neurophysiological interfaces such as EEG devices, including systems in the class of Neurosity -A recommendation was not supposed to be the end of the loop. +The goal was **not** to make any sensor authoritative. -It was a **claim** about what the operator should do next. - -That claim could later be compared with the outcome. +The intended model was: ```text -observation → inference → intervention → outcome → calibration +imperfect sensor + ↓ +observation + ↓ +derived feature + ↓ +evidence + ↓ +state hypothesis + ↓ +policy + ↓ +outcome + ↓ +calibration ``` -That feedback loop is the important part. +A future EEG integration would therefore contribute observations or derived features. It would not directly declare a cognitive state or diagnosis. --- ## Architecture -The repository separates the system into bounded contexts rather than storing everything inside generic notes or chat history. +The cleaned architecture is documented in: -### Vector +- [Architecture overview](docs/architecture/overview.md) +- [RFC-0001: Neurocognitive System](docs/rfcs/RFC-0001-neurocognitive-system.md) +- [RFC-0002: Observation, Evidence, and Inference](docs/rfcs/RFC-0002-observation-evidence-inference.md) +- [Sensor architecture](docs/architecture/sensors.md) +- [RFC-0003: Sensor Adapter Contract](docs/rfcs/RFC-0003-sensor-adapter-contract.md) +- [Historical terminology](docs/history/terminology.md) +- [Static codebase audit](docs/audit/2026-09-23.md) -Directional control. +The original Utopia documents are preserved unchanged under [docs/history](docs/history/README.md). -Vector models what matters across different horizons: +--- -```text -life arc → season → mission → thread -``` +## Core primitives -It exists to prevent motion from being mistaken for progress. +### Direction -### Evidence +Historical name: **Vector**. -The sensing layer. +Direction represents what matters: -It captures subjective, behavioral, contextual, and derived evidence about the operator's present condition. +```text +life arc → season → mission → thread +``` -The governing principle was: +It exists to distinguish useful motion from drift. -**evidence before narrative.** +### Sensing and Evidence -The system should not automatically treat a present-state self-interpretation as ground truth. +The system records subjective, behavioral, contextual, and physiological signals. -### Physiology +The cleaned architecture separates: -A physiological evidence channel. +**observation ≠ evidence ≠ inference** -The implementation includes WHOOP ingestion for cycles, sleep, recovery, workouts, body measurements, and derived physiology features. +A measurement is not automatically a conclusion. -Physiological data was intended to constrain interpretation, not define identity. +### Inference -### Execution +The historical AI Fabric estimates things such as: -The action-control layer. +- current operating state +- dominant blocker +- contradictions between narrative and evidence -Execution stores: +Inference should remain explicit about confidence, provenance, and failure. -- state estimates -- blocker estimates -- policy decisions -- re-entry artifacts -- traces +### Policy -The important chain is: +Historical name: **Schrödinger**. -```text -evidence → inference → policy → outcome -``` +Policy selects an intervention and action depth based on available evidence and inferred state. -A policy decision can be traced back to the estimates that produced it, and a later trace can record what actually happened. +A policy decision is a hypothesis about what to try next, not ground truth. ### Re-entry -Interruptions were treated as normal rather than exceptional. +Interrupted work is treated as normal. -A re-entry artifact preserves enough state to restart a thread without reconstructing the entire problem: +A re-entry artifact preserves: - last completed step - unresolved edge - smallest next move - trap to avoid - relevant context +- freshness -Re-entry was modeled as a first-class object because continuity loss was considered part of the system, not an edge case. +### Memory -### Aether +Historical name: **Aether**. -Typed memory and knowledge. - -Aether was designed to preserve reusable cognitive objects rather than accumulate an undifferentiated archive. - -Its model includes concepts such as: +The system stores typed cognitive objects rather than treating all memory as notes: - concepts - mechanisms @@ -192,289 +191,125 @@ Its model includes concepts such as: - cases - rules - patterns -- explicit edges between them - -The intention was to make memory useful for future judgment rather than merely searchable. - -### Reasoning - -Structured problem representation. - -The reasoning layer stores artifacts such as: - -- problems -- problem structures -- interrogations -- decision briefs -- option paths -- contradiction reports - -The underlying idea was that the system should improve the shape of the problem before rushing to solve it. - -### Review and Calibration - -The system's correction layer. - -Review stores: - -- closures -- review sessions -- rule promotions -- pattern updates -- calibration records - -Calibration records were designed to compare earlier estimates with later outcomes. - -That makes it possible, at least architecturally, for the system to ask: - -**Was my previous interpretation actually accurate?** - -### System Audit - -The AI and retrieval audit layer records model runs, retrieval runs, events, and outbox state so that reasoning does not disappear into an opaque chat transcript. - ---- - -## AI Fabric - -The implementation contains nine reasoning modules. - -### Core execution pipeline - -- **State Estimator** — estimates the operator's current operating state. -- **Blocker Classifier** — identifies the dominant reason motion is failing. -- **Policy Selector** — chooses an intervention and action depth appropriate to the estimated state. - -### Extended reasoning - -- **Router** — classifies incoming intent and chooses the appropriate reasoning path. -- **Problem Structurer** — turns raw problems into explicit objectives, constraints, unknowns, assumptions, and bottlenecks. -- **Context Retriever** — retrieves relevant material from the typed knowledge graph. -- **Physiology Interpreter** — translates physiological signals into bounded capacity information. -- **Contradiction Checker** — compares narrative against behavior, physiology, direction, and temporal patterns. -- **Council** — runs multiple reasoning lenses and synthesizes agreement and tension. - -Claude is used as the reasoning backend for these modules. OpenAI embeddings are used for semantic retrieval. +- explicit graph edges -The model layer was intended to remain subordinate to the typed system around it. +### Feedback and Calibration ---- - -## What was actually built +The historical implementation records traces, reviews, rule promotions, pattern updates, and calibration records. -This repository is more than an architecture document. - -The historical implementation includes: - -- Python 3.12+ -- FastAPI -- SQLAlchemy 2.x async -- PostgreSQL 16 -- pgvector -- Alembic -- 13 database migrations -- typed ORM models and Pydantic schemas -- bounded service layers -- API routes across the major contexts -- WHOOP API client, mapping, and synchronization -- semantic vector search -- Claude-backed reasoning modules -- OpenAI embedding support -- system-level model/retrieval audit records -- three materialized views for current state, active focus, and thread priority -- service, reasoning-module, and API-route tests - -The major bounded contexts represented in code are: +The intended end-state was a real feedback loop: ```text -core -integration -vector_ctrl -evidence -execution -physiology -aether -reasoning -review -system -vector +prediction → action → outcome → error → reliability update ``` ---- - -## Implementation vs. architecture - -The architecture documents in this repository describe a larger intended system than the implementation reached. - -That distinction is deliberate. - -Some ideas were implemented deeply enough to have migrations, models, services, routes, and tests. - -Others remained design directions. - -The documents are preserved because they show the evolution of the system's reasoning, but they should not be read as claims that every proposed subsystem became a finished product. - -There is no attempt here to rewrite the historical record into a cleaner story than it was. +The historical code models this loop more completely than it executes it. --- -## Architectural principles - -Several ideas mattered more than any individual feature. - -### State before planning - -The correct next action depends on the state of the operator executing it. - -### Evidence before narrative - -Self-interpretation is evidence, not unquestionable truth. - -### Direction before motion - -Activity is not inherently progress. - -### Re-entry as a first-class object - -A system designed for interruption should preserve the minimum state required to resume. - -### Typed memory over accumulation - -If everything is a note, the system loses semantic structure. +## What is implemented vs. what was intended -### Explicit confidence and provenance +This repository intentionally distinguishes implementation from architecture. -Inferred state should remain distinguishable from observed fact. +### Implemented -### Outcomes over persuasive explanations +Substantial backend/domain infrastructure exists for: -A recommendation that sounds intelligent is still only a hypothesis until reality answers. +- Direction / Vector +- Evidence +- Execution +- Physiology +- Aether +- Reasoning +- Review +- System Audit +- vector search +- WHOOP ingestion +- AI reasoning modules -### Calibration over permanent assumptions +### Incomplete or architectural -The system should be able to compare previous estimates with subsequent outcomes and update accordingly. +Examples include: -### Separation of observation, judgment, and execution +- a complete authentication/authorization boundary +- automatic model/retrieval audit wiring +- a full encrypted OAuth/token lifecycle +- automatic materialized-view refresh +- calibration automatically changing future model/rule weighting +- the complete cross-domain control loop +- general sensor-adapter abstractions +- EEG / Neurosity integration -Sensing reality, interpreting reality, and acting on reality are different operations and should remain inspectable. +These are documented as unfinished rather than presented as completed features. --- -## Why preserve this repository? +## Trust boundary -Neurocognitive System is not the architecture I would build unchanged today. +**Do not expose the historical API directly to an untrusted network.** -That is part of its value. +The current implementation was built as private prototype software and does not contain a complete authentication and authorization boundary. -It preserves an earlier attempt to formalize several problems that continued to matter: - -- continuity across interrupted work -- explicit system state -- evidence-backed inference -- re-entry cost -- typed memory -- confidence and provenance -- contradiction detection -- action selection under variable capacity -- outcome traces -- calibration -- review as a correction mechanism - -Looking backward, the vocabulary changed more than the underlying questions. - -The repository is useful as architecture archaeology: a record of how those ideas were represented before later systems refined them. - -**The implementation is historical. The questions were not.** +See [SECURITY.md](SECURITY.md). --- -## Historical terminology - -Some names in the code and architecture documents are preserved because they were part of the original system. +## Medical boundary -- **Utopia** — original project codename -- **Vector** — directional control plane -- **Aether** — typed knowledge and memory -- **Schrödinger** — original name for the policy-selection / "one correct move" concept +Neurocognitive System is **not a medical diagnostic system**. -These names should be read as historical terminology, not as a recommendation for how the same primitives would necessarily be named today. +Physiological or future neurophysiological inputs are treated as imperfect evidence about operational state and usable capacity. They are not a basis for diagnosing medical or psychiatric conditions. --- -## Repository structure +## Repository layout ```text +src/utopia/ + api/ # historical FastAPI surface + models/ # SQLAlchemy domain model + schemas/ # Pydantic contracts + services/ # bounded-context services + integrations/whoop/ # implemented sensor integration + ai/ # historical reasoning runtime + migrations/ - versions/ # 13 historical Alembic migrations + versions/ # historical migrations + cleanup migrations -src/utopia/ - api/ # FastAPI application and routes - models/ # SQLAlchemy domain models - schemas/ # Pydantic request/response contracts - services/ # bounded-context service layer - integrations/ - whoop/ # WHOOP client, mapping, sync - ai/ # reasoning modules and providers - config.py - db.py - enums.py +docs/ + architecture/ # cleaned architecture + rfcs/ # redesign contracts + audit/ # static audit record + history/ # original Utopia design material tests/ - conftest.py test_services.py test_ai_modules.py test_routes.py - -docs/ - adhd-visual-prosthetic-thesis.md - -Utopia Architecture.md -Utopia Formal Architecture DB etc.md ``` --- -## Local setup +## Local inspection -This is a historical repository, so setup instructions are preserved primarily for inspection and experimentation. - -### Requirements - -- Python 3.12+ -- Docker / Docker Compose -- PostgreSQL 16 with pgvector - -### Environment - -Copy the example environment file and provide only the integrations you intend to use: +This repository is preserved primarily for architecture inspection and experimentation. ```bash cp .env.example .env -``` - -Available configuration includes PostgreSQL, WHOOP, Anthropic, and OpenAI credentials. - -### Database - -```bash docker compose up -d +pip install -e ".[dev]" alembic upgrade head +pytest ``` -### Development - -Install the project and run the test suite using the configuration in `pyproject.toml`. +Provider keys are optional unless exercising the corresponding external integrations. --- ## Status -**Historical / inactive / preserved.** - -There is no active product roadmap for this repository. - -Parts of the architecture may be extracted, rewritten, or reappear elsewhere, but this repository represents the system as it existed during this experiment. +There is no active product roadmap for the historical Utopia application. -It is preserved as evidence of the design process, including ideas that worked, ideas that did not, and primitives that survived into later thinking. +The cleanup exists to expose the durable architectural primitives without rewriting the prototype into something it never was. **The implementation is historical. The questions were not.** diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..29c298b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,45 @@ +# Security + +## Status + +This repository contains a **historical private-system prototype**. + +It is not production-hardened and should not be exposed directly to an untrusted network. + +## Current trust model + +The historical FastAPI implementation does not contain a complete authentication or authorization boundary. + +Until that is redesigned: + +- bind services to a trusted local/private interface only +- do not expose the API publicly +- do not use production credentials +- do not store irreplaceable private data in an unreviewed deployment +- keep provider credentials in environment variables, never in the repository + +## Sensitive data + +The architecture can contain highly sensitive information, including: + +- subjective check-ins +- behavior traces +- physiological data +- biomarker information +- reasoning artifacts +- imported personal knowledge +- future neurophysiological observations + +A future deployment should treat these data classes as private by default. + +## Future sensor integrations + +Planned sensor-agnostic architecture may support sources such as EEG devices. + +Raw neurophysiological data should not automatically be forwarded to external model providers. Sensor data should be normalized locally into explicit observations/features with provenance and retention controls. + +## Medical boundary + +This project is not a medical diagnostic system. + +Physiological and neurophysiological signals should be treated as imperfect evidence about operational state, not as diagnostic truth. diff --git a/alembic.ini b/alembic.ini index d76a706..9efd1f7 100644 --- a/alembic.ini +++ b/alembic.ini @@ -1,6 +1,6 @@ [alembic] script_location = migrations -sqlalchemy.url = postgresql+psycopg2://utopia:utopia@localhost:5432/utopia +sqlalchemy.url = postgresql+psycopg://utopia:utopia@localhost:5432/utopia [loggers] keys = root,sqlalchemy,alembic diff --git a/docker-compose.yml b/docker-compose.yml index 2edddb3..bd98590 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: POSTGRES_PASSWORD: utopia POSTGRES_DB: utopia ports: - - "5432:5432" + - "127.0.0.1:5432:5432" volumes: - pgdata:/var/lib/postgresql/data healthcheck: diff --git a/docs/adhd-visual-prosthetic-thesis.md b/docs/adhd-visual-prosthetic-thesis.md deleted file mode 100644 index 7ba12d5..0000000 --- a/docs/adhd-visual-prosthetic-thesis.md +++ /dev/null @@ -1,256 +0,0 @@ -# ADHD as a Continuity Problem - -## A thesis for AR as a visual prosthetic - -### TL;DR - -ADHD is often framed as a motivation problem, a discipline problem, or a willpower problem. - -That is not the most useful frame. - -A better frame is this: - -**ADHD is, in part, a continuity failure problem.** - -The issue is not always knowing what matters. -The issue is keeping what matters perceptually alive long enough for action to remain aligned with intention. - -In that sense, AR glasses may become more than a display layer or convenience device. -They may become a **visual prosthetic for priority persistence**. - ---- - -## The core idea - -For many people with ADHD, the main objective does not remain stably present in working consciousness. - -The mind does not only get distracted by trivial things. -It gets captured by whatever becomes most immediate, most stimulating, most novel, or most visible. - -This creates a recurring pattern: - -1. The person knows the real priority. -2. Attention drifts. -3. A lower-value but more immediate task captures the system. -4. Time passes under false importance. -5. The person "returns" and realizes they were not acting on the true objective. - -This is not merely forgetfulness. -It is a breakdown in **priority persistence**. - -If the problem is partly perceptual, then part of the solution may also be perceptual. - ---- - -## Why vision matters - -Attention is heavily shaped by what remains in the visual field. - -What stays visible has a structural advantage over what must be re-remembered. -A priority that must be mentally reloaded is fragile. -A priority that is continuously and intelligently present in the line of sight is more stable. - -This suggests a simple but powerful hypothesis: - -**If the most important task, constraint, or objective could remain perceptually present at the edge of awareness, some forms of ADHD-related drift may be reduced.** - -Not cured. -Reduced. - -The mechanism is not magical. -It is environmental scaffolding applied directly to the attentional bottleneck. - ---- - -## AR glasses as a prosthetic layer - -Most productivity tools assume the user will remember to check them. - -That assumption breaks under attentional volatility. - -Phones, task managers, notes apps, and calendar alerts all rely on re-entry: - -- open the app -- remember the context -- reconstruct the priority -- resume motion - -For people with ADHD, that reload cost is often exactly where continuity breaks. - -AR changes the interface model. - -Instead of requiring repeated re-entry, the system can place the correct cue directly into perception. - -That means AR glasses could function as a prosthetic layer for: - -- main objective persistence -- task hierarchy visibility -- interruption recovery -- anti-drift signaling -- contextual reminders at the moment of deviation - -The point is not to fill vision with noise. -The point is to create a **minimal visual anchor** that protects the top priority from disappearing. - ---- - -## Prosthetic, not enhancement - -The word *prosthetic* matters. - -A prosthetic does not make someone superhuman. -It compensates for a recurring functional gap. - -In this case, the gap is not intelligence. -It is not effort. -It is not moral weakness. - -The gap is the unstable persistence of the correct target inside consciousness. - -So the proposition is not: - -"AR glasses make people more productive." - -The proposition is: - -**For some ADHD users, AR glasses may serve as an attentional prosthetic by keeping the correct target visually available when internal continuity fails.** - -That is a much stronger idea. - ---- - -## A simple operating model - -An ADHD visual prosthetic could work through a few basic rules. - -### 1. One objective, always visible -A single line in the display: - -- current objective -- current highest-value action -- current anti-goal - -Not ten tasks. -Not a dashboard. -One live anchor. - -### 2. Drift detection -If the user opens unrelated apps, changes context too often, stalls, or loops, the system infers drift. - -### 3. Re-entry prompts -When drift is detected, the display surfaces a tiny corrective cue: - -- `Main objective: finish X` -- `This is not the priority` -- `Return to the next smallest move` - -### 4. State-aware adaptation -If the user is depleted, overwhelmed, or fragmented, the system changes the prompt depth. - -Not: -- "Finish the project" - -But: -- "Open the file" -- "Write one sentence" -- "Send one reply" - -### 5. Constraint visibility -The display can also keep a live constraint present: - -- do not switch tasks -- no new tabs -- finish before exploring -- stay with the bottleneck - -This is less about reminder technology and more about **real-time cognitive stabilization**. - ---- - -## What this solves - -This kind of system may help with: - -- losing the real priority after interruptions -- false urgency replacing real importance -- hyperfixating on low-output side quests -- context collapse between intention and action -- friction of reloading the task stack from memory - -In plain English: - -It may help the user keep the true thing visible long enough to act on it. - ---- - -## What it does not solve - -This thesis should not be exaggerated. - -A visual prosthetic would not solve every dimension of ADHD. -It would not fix sleep, emotional dysregulation, under-stimulation, shame loops, avoidance, or deeper executive dysfunction by itself. - -But that is not a weakness of the idea. -It is exactly why the frame is useful. - -A good prosthetic does not need to solve everything. -It only needs to reliably support one broken part of the loop. - -And if that broken part is **priority persistence**, then the leverage may be enormous. - ---- - -## Why this matters now - -AR hardware is often marketed through entertainment, novelty, or generic future-tech language. - -But one of the strongest near-term categories may be cognitive assistance. -Not in the abstract. -Not as hype. -But in practical, narrow, repeatable use cases. - -ADHD is one of the clearest candidates because: - -- the problem is frequent -- the problem is daily -- the problem is experiential and felt -- the problem is partly perceptual and contextual -- the intervention can be minimal but continuous - -This makes AR less like a toy and more like interface infrastructure for cognition. - ---- - -## Product direction - -A serious prototype would likely need to be: - -- minimal rather than immersive -- persistent rather than flashy -- state-aware rather than static -- corrective rather than distracting -- personalized rather than generic - -The wrong product would feel like more noise. -The right product would feel like **a calm second layer of executive function**. - -That is the design challenge. - ---- - -## Final claim - -The real promise of AR for ADHD may not be information access. -It may be **objective persistence**. - -Not more data. -Not more stimulation. -Not another app. - -A perceptual support layer that keeps the right thing alive in the line of sight when the mind would otherwise drop it. - -That is why AR glasses may become a genuine visual prosthetic for some people with ADHD. - -And if that is true, then the category is much bigger than smart glasses. - -It is cognitive infrastructure. diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md new file mode 100644 index 0000000..c69f31c --- /dev/null +++ b/docs/architecture/overview.md @@ -0,0 +1,206 @@ +# Neurocognitive System Architecture + +Neurocognitive System is the cleaned architectural interpretation of the historical Utopia prototype. + +The system is organized around one control loop: + +```text +Direction + ↓ +Sensing + ↓ +Observations + ↓ +Features + ↓ +Evidence Bundle + ↓ +Inference + ↓ +Policy + ↓ +Action / Re-entry + ↓ +Outcome Trace + ↓ +Review + ↓ +Calibration + ↺ +``` + +The key design rule is that these stages are not interchangeable. + +A sensor reading is not an inference. +An inference is not a policy. +A policy is not truth. +An outcome is evidence about whether the prior policy and inference were useful. + +## 1. Direction + +Direction defines what the system is trying to preserve or advance. + +Historical Utopia terminology called this layer **Vector**. + +The current implementation models: + +```text +life arc → season → mission → thread +``` + +Direction exists to distinguish useful motion from drift. + +## 2. Sensing + +Sensing is intentionally modality-agnostic. + +Historical implementation includes: + +- subjective check-ins +- behavioral events +- contextual snapshots +- WHOOP physiology + +The intended architecture was broader and was designed to support future sensor adapters, including neurophysiological sources such as EEG devices. + +A sensor adapter should emit normalized observations. It should never directly emit a domain conclusion such as "the operator is depleted." + +## 3. Observations + +An observation is a recorded measurement, report, or event. + +Examples: + +- subjective energy = 35 +- failed-start event +- WHOOP recovery measurement +- environment interruption count +- future EEG-derived feature window + +Observations preserve source, time, provenance, and freshness. + +## 4. Features + +Features are transformations over observations. + +Examples: + +- failed-start rate over 24 hours +- recovery trend +- re-entry risk +- context-switch density +- EEG-derived signal features + +Features remain measurements. They are not state labels. + +## 5. Evidence Bundles + +An evidence bundle is the exact set of observations and features used to evaluate a specific claim. + +This creates a stable boundary between sensing and reasoning and makes the system auditable. + +## 6. Inference + +Inference turns evidence into explicit hypotheses. + +Historical examples include: + +- state estimate +- blocker estimate +- contradiction report + +Every inference should eventually carry: + +- status +- confidence +- provenance +- evidence references +- model/rule version +- timestamp + +Provider failure or parse failure must not silently become a domain belief. + +## 7. Policy + +Policy proposes the next intervention given: + +- direction +- current evidence +- inferred state +- blocker hypothesis +- available capacity +- re-entry context +- constraints + +Historical Utopia called the policy selector **Schrödinger**. + +A policy decision is a testable proposal, not an oracle. + +## 8. Re-entry + +Re-entry remains a first-class concept. + +A re-entry artifact should preserve the minimum state necessary to resume an interrupted thread: + +- last completed step +- unresolved edge +- smallest next move +- relevant context +- trap to avoid +- freshness + +## 9. Outcome and Trace + +A trace records what actually happened after a policy was acted upon. + +The system should preserve the causal chain: + +```text +evidence → inference → policy → action → outcome +``` + +## 10. Review and Calibration + +Calibration compares predictions with outcomes. + +The historical implementation contains review sessions, rule promotions, pattern updates, and calibration records. The cleaned architecture treats those records as the beginning of a real feedback loop rather than the completion of one. + +The intended loop is: + +```text +prediction + ↓ +action + ↓ +outcome + ↓ +error / usefulness measurement + ↓ +reliability update + ↓ +future evidence weighting +``` + +## 11. Memory and Reasoning + +Historical **Aether** is the typed memory layer. It stores structured cognitive objects rather than an undifferentiated note archive. + +The reasoning layer stores explicit problem structures, unknowns, assumptions, decision briefs, options, and contradiction reports. + +Retrieved knowledge is evidence. It must never be treated as trusted instruction merely because it was retrieved. + +## 12. Trust boundary + +The historical codebase is a private prototype, not production multi-tenant software. + +The current API does not implement a complete authentication or authorization boundary. Do not expose it to untrusted networks. + +See [SECURITY.md](../../SECURITY.md). + +## 13. Medical boundary + +Neurocognitive System is designed around operational state estimation and continuity. + +It is not a medical diagnostic system. + +Physiological or future neurophysiological inputs should be treated as noisy evidence about operational capacity, not as a basis for diagnosing medical or psychiatric conditions. diff --git a/docs/architecture/sensors.md b/docs/architecture/sensors.md new file mode 100644 index 0000000..ee81a42 --- /dev/null +++ b/docs/architecture/sensors.md @@ -0,0 +1,120 @@ +# Sensor architecture + +The cleaned Neurocognitive System treats sensing as a replaceable boundary. + +The historical implementation contains WHOOP-specific ingestion. Future sensing should be implemented through a common adapter contract. + +## Design rule + +A sensor adapter emits **observations**. + +It does not emit diagnoses, identity claims, or policy decisions. + +```text +device / source + ↓ +adapter + ↓ +observation + ↓ +feature derivation + ↓ +evidence bundle + ↓ +inference +``` + +## Proposed SensorAdapter contract + +A future adapter should expose capabilities such as: + +- source identity +- device/session identity +- observation types +- time range +- sampling metadata +- freshness +- provenance +- optional raw-artifact reference + +Conceptually: + +```python +class SensorAdapter(Protocol): + source_kind: str + + async def capabilities(self) -> SensorCapabilities: ... + async def collect(self, request: CollectionRequest) -> list[Observation]: ... +``` + +The exact implementation is intentionally deferred. + +## High-frequency sources + +High-frequency sources such as EEG should not be represented as one PostgreSQL row per raw sample. + +The intended split is: + +```text +SensorSession + ↓ +raw artifact / stream + ↓ +feature windows + ↓ +normalized observations + ↓ +evidence +``` + +Raw signal artifacts can live in private object/file storage. PostgreSQL should store identity, provenance, time ranges, checksums, and derived features required by the control loop. + +## WHOOP + +WHOOP is the historical first sensor implementation. + +In the cleaned architecture it should eventually become an adapter that produces observations such as: + +- recovery +- sleep +- strain/cycle +- workout +- body measurement + +WHOOP-derived values remain evidence inputs, not policy authority. + +## EEG / neurophysiology + +The historical direction included future neurophysiological sensing, including devices in the class of Neurosity EEG. + +A future EEG adapter should preserve: + +- device identity +- session identity +- sample-rate/channel metadata +- time range +- processing pipeline/version +- raw-artifact checksum/reference +- derived feature definitions +- feature-window timestamps +- quality/confidence indicators + +EEG-derived features must not be translated directly into medical or psychiatric conclusions. + +The useful question for this architecture is operational: + +> Does this signal improve prediction of usable cognitive state or action depth for this operator? + +If repeated outcomes show that a signal is not useful, the calibration layer should reduce its weight. + +## Privacy + +Raw neurophysiological data is a sensitive data class. + +Default policy should be: + +- private/local storage +- explicit retention +- no automatic external-model upload +- derived features preferred over raw streams for routine inference +- provenance retained for every transformation diff --git a/docs/audit/2026-09-23.md b/docs/audit/2026-09-23.md new file mode 100644 index 0000000..1b7a390 --- /dev/null +++ b/docs/audit/2026-09-23.md @@ -0,0 +1,66 @@ +# Static audit — 2026-09-23 + +This is a static audit of the historical Utopia codebase prior to the Neurocognitive System cleanup. + +## Strong existing primitives + +- explicit directional hierarchy +- separate evidence, inference, policy, trace, and review concepts +- re-entry as a first-class object +- typed memory rather than a generic notes store +- PostgreSQL + pgvector architecture +- structured AI modules +- WHOOP ingestion +- model/retrieval audit data models +- calibration data model +- reproducible migration history + +## Important gaps + +### Trust boundary + +The FastAPI surface does not currently implement a complete authentication and authorization boundary. + +The code should be treated as local/private prototype software. + +### Operator ownership + +Many lookups use bare object IDs rather than enforcing ownership by operator. Nested routes also historically allowed path IDs and body IDs to disagree. + +### Vector isolation + +Semantic retrieval historically allowed operator scoping to be omitted in some paths. + +### Inference failure semantics + +Several AI modules historically convert parse failure into low-confidence fallback domain values. Future architecture should preserve failure as failure. + +### Incomplete control loop + +The main assessment loop currently uses evidence and execution services, but does not automatically integrate all of Direction, stored Physiology, Memory, Re-entry, and Calibration. + +### Calibration + +Calibration objects are persisted, but calibration does not yet automatically change later model/rule weighting. + +### Audit wiring + +Model-run and retrieval-run tables exist, but model invocations are not automatically written to them. + +### Materialized views + +Three materialized views exist, but the repository does not implement an automatic refresh mechanism. + +### OAuth + +OAuth/token storage models exist, but a complete encrypted OAuth lifecycle and refresh flow is not implemented. WHOOP sync currently accepts an access token directly. + +### Verification + +The test suite covers important core paths but does not yet cover the full bounded-context surface. + +## Cleanup rule + +The redesign should fix high-confidence correctness problems while preserving the repository as an honest historical artifact. + +It should not retroactively claim production readiness. diff --git a/docs/history/README.md b/docs/history/README.md new file mode 100644 index 0000000..377b757 --- /dev/null +++ b/docs/history/README.md @@ -0,0 +1,11 @@ +# Historical Utopia material + +This directory preserves the original architecture documents from the Utopia prototype. + +They are intentionally retained with their original language, assumptions, and terminology. + +- [Original architecture](original-architecture.md) +- [Original database design](original-database-design.md) +- [Terminology map](terminology.md) + +These files are historical evidence, not statements that every described subsystem was implemented. diff --git a/Utopia Architecture.md b/docs/history/original-architecture.md similarity index 100% rename from Utopia Architecture.md rename to docs/history/original-architecture.md diff --git a/Utopia Formal Architecture DB etc.md b/docs/history/original-database-design.md similarity index 100% rename from Utopia Formal Architecture DB etc.md rename to docs/history/original-database-design.md diff --git a/docs/history/terminology.md b/docs/history/terminology.md new file mode 100644 index 0000000..99dd32c --- /dev/null +++ b/docs/history/terminology.md @@ -0,0 +1,18 @@ +# Historical terminology + +The repository began under the codename **Utopia**. Several internal names are preserved in code and historical documents. + +| Historical term | Cleaned interpretation | +| --- | --- | +| Utopia | Neurocognitive System | +| Vector | Direction | +| Evidence Plane | Sensing + Evidence | +| Aether | Typed Memory | +| AI Fabric | Inference / Reasoning Runtime | +| Schrödinger | Policy Selector | +| Trace | Outcome / execution trace | +| Review | Feedback + Calibration | + +The historical terminology is not being erased. It is kept so the original implementation and documents remain understandable. + +The cleaned vocabulary exists to make architectural boundaries more explicit. diff --git a/docs/rfcs/RFC-0001-neurocognitive-system.md b/docs/rfcs/RFC-0001-neurocognitive-system.md new file mode 100644 index 0000000..50a5a7b --- /dev/null +++ b/docs/rfcs/RFC-0001-neurocognitive-system.md @@ -0,0 +1,49 @@ +# RFC-0001: Neurocognitive System + +**Status:** Draft +**Historical predecessor:** Utopia v0.1 + +## Summary + +This RFC defines the cleaned architecture extracted from Utopia. + +The historical implementation explored continuity, state-aware action, typed memory, physiology, reasoning, traces, and calibration. The redesign keeps the durable primitives while separating sensing, evidence, inference, policy, and feedback more rigorously. + +## Core claim + +The operator is not stable. + +Attention, context, energy, environment, and usable cognitive capacity change over time. A control system should therefore estimate state from imperfect evidence rather than assume a permanently stable executor. + +## System loop + +```text +Direction → Sensing → Observations → Features → Evidence + → Inference → Policy → Action → Outcome → Calibration +``` + +## Non-goals + +This system does not: + +- diagnose medical or psychiatric conditions +- treat any wearable or EEG signal as ground truth +- make model output equivalent to truth +- replace operator authority +- optimize activity without regard to direction + +## Historical compatibility + +The existing Python package remains `utopia` during the cleanup phase so the historical implementation remains inspectable. + +Historical names such as Vector, Aether, and Schrödinger are documented but are not required vocabulary for future architecture. + +## Migration principle + +Do not rewrite history to make the prototype appear more complete than it was. + +The cleaned architecture must clearly distinguish: + +1. implemented historical behavior +2. intended historical architecture +3. proposed future abstractions diff --git a/docs/rfcs/RFC-0002-observation-evidence-inference.md b/docs/rfcs/RFC-0002-observation-evidence-inference.md new file mode 100644 index 0000000..0320e70 --- /dev/null +++ b/docs/rfcs/RFC-0002-observation-evidence-inference.md @@ -0,0 +1,82 @@ +# RFC-0002: Observation, Evidence, and Inference + +**Status:** Draft + +## Problem + +The historical architecture sometimes collapses measurement, evidence, and model judgment into one object graph. + +That makes provenance harder to inspect and allows model failure to masquerade as domain truth. + +## Definitions + +### Observation + +A measurement, report, or event produced by a known source. + +An observation answers: + +> What was recorded? + +### Feature + +A deterministic or model-derived transformation over one or more observations. + +A feature answers: + +> What signal was derived? + +### Evidence + +An observation or feature used in support of or against a specific claim. + +Evidence answers: + +> Why is this claim being considered? + +### Inference + +A hypothesis produced from an evidence bundle. + +An inference answers: + +> What does the system currently believe, with what confidence and provenance? + +### Policy + +A proposed intervention based on direction, evidence, inference, constraints, and capacity. + +A policy answers: + +> What should be tried next? + +### Outcome + +A trace of what happened after action. + +An outcome answers: + +> What did reality do? + +## Required invariant + +No provider error, parse error, missing sensor value, or retrieval failure may silently become a valid inference. + +Future inference records should distinguish statuses such as: + +- valid +- low_confidence +- insufficient_evidence +- provider_error +- parse_error +- rejected + +## Sensor rule + +Sensor adapters emit observations. + +They do not emit operator identity claims, diagnoses, or policy decisions. + +## Calibration rule + +Confidence should become increasingly grounded in observed predictive usefulness rather than model self-reported certainty alone. diff --git a/docs/rfcs/RFC-0003-sensor-adapter-contract.md b/docs/rfcs/RFC-0003-sensor-adapter-contract.md new file mode 100644 index 0000000..16dcb55 --- /dev/null +++ b/docs/rfcs/RFC-0003-sensor-adapter-contract.md @@ -0,0 +1,48 @@ +# RFC-0003: Sensor Adapter Contract + +**Status:** Draft + +## Motivation + +The historical Utopia implementation contains WHOOP-specific integration logic, while the intended system was multimodal. + +A general sensor contract prevents the core architecture from becoming coupled to one wearable or one modality. + +## Contract boundary + +Adapters convert external sources into normalized observations. + +They MUST NOT: + +- declare operator state +- choose policy +- infer diagnosis +- silently discard provenance + +They SHOULD expose: + +- source kind +- source/device identifier +- collection/session identifier +- observed-at timestamps or ranges +- raw value/payload or artifact reference +- units +- quality metadata +- adapter/version metadata + +## High-frequency data + +High-frequency streams such as EEG should use a session + artifact + feature-window model instead of row-per-sample storage in the relational core. + +## Historical implementation + +WHOOP remains the first concrete integration. + +Future examples may include: + +- EEG systems such as Neurosity-class devices +- additional wearables +- environment sensors +- software/behavior telemetry + +These are extension points, not claims of current implementation. diff --git a/migrations/env.py b/migrations/env.py index f0492e0..54ae4d8 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -5,12 +5,18 @@ from alembic import context from sqlalchemy import engine_from_config, pool +from utopia.config import settings + config = context.config + +# Keep one source of truth for migration connectivity. The historical +# alembic.ini value remains a local fallback, while .env can override it. +config.set_main_option("sqlalchemy.url", settings.database_url_sync) + if config.config_file_name is not None: fileConfig(config.config_file_name) -# We don't use autogenerate in this project — migrations are hand-written -# to match the architecture docs precisely. +# Historical migrations are hand-written; autogenerate is intentionally disabled. target_metadata = None diff --git a/migrations/versions/003_integration_tables.py b/migrations/versions/003_integration_tables.py index 3905bdc..97db0c8 100644 --- a/migrations/versions/003_integration_tables.py +++ b/migrations/versions/003_integration_tables.py @@ -12,7 +12,7 @@ from alembic import op import sqlalchemy as sa -from sqlalchemy.dialects.postgresql import ARRAY, BYTEA, JSONB +from sqlalchemy.dialects.postgresql import ARRAY, BYTEA, ENUM, JSONB revision: str = "003" down_revision: Union[str, None] = "002" @@ -31,7 +31,7 @@ def upgrade() -> None: sa.Column("scopes", ARRAY(sa.Text()), nullable=False, server_default="{}"), sa.Column( "status", - sa.Enum("active", "revoked", "expired", name="oauth_status", schema="core", create_type=False), + ENUM("active", "revoked", "expired", name="oauth_status", schema="core", create_type=False), nullable=False, ), sa.Column("encrypted_access_token", BYTEA(), nullable=True), @@ -81,7 +81,7 @@ def upgrade() -> None: sa.Column("processed_at", sa.DateTime(timezone=True), nullable=True), sa.Column( "processing_status", - sa.Enum("pending", "processed", "failed", name="processing_status", schema="core", create_type=False), + ENUM("pending", "processed", "failed", name="processing_status", schema="core", create_type=False), nullable=False, server_default="pending", ), diff --git a/migrations/versions/004_vector_ctrl_tables.py b/migrations/versions/004_vector_ctrl_tables.py index 662cf39..c5bac0a 100644 --- a/migrations/versions/004_vector_ctrl_tables.py +++ b/migrations/versions/004_vector_ctrl_tables.py @@ -13,7 +13,7 @@ from alembic import op import sqlalchemy as sa -from sqlalchemy.dialects.postgresql import ARRAY, JSONB +from sqlalchemy.dialects.postgresql import ARRAY, ENUM, JSONB revision: str = "004" down_revision: Union[str, None] = "003" @@ -31,7 +31,7 @@ def upgrade() -> None: sa.Column("description", sa.Text(), nullable=True), sa.Column( "status", - sa.Enum("active", "paused", "completed", "abandoned", "archived", "dormant", + ENUM("active", "paused", "completed", "abandoned", "archived", "dormant", name="status", schema="core", create_type=False), nullable=False, ), @@ -61,7 +61,7 @@ def upgrade() -> None: sa.Column("priority_stack", JSONB(), nullable=False, server_default="[]"), sa.Column( "status", - sa.Enum("planned", "active", "closed", + ENUM("planned", "active", "closed", name="season_status", schema="core", create_type=False), nullable=False, ), @@ -84,14 +84,14 @@ def upgrade() -> None: sa.Column("description", sa.Text(), nullable=True), sa.Column( "mission_kind", - sa.Enum("strategic", "technical", "personal", "recovery", "exploratory", + ENUM("strategic", "technical", "personal", "recovery", "exploratory", name="mission_kind", schema="core", create_type=False), nullable=False, ), sa.Column("priority_score", sa.Numeric(6, 3), nullable=True), sa.Column( "status", - sa.Enum("active", "paused", "completed", "abandoned", "archived", "dormant", + ENUM("active", "paused", "completed", "abandoned", "archived", "dormant", name="status", schema="core", create_type=False), nullable=False, ), @@ -122,13 +122,13 @@ def upgrade() -> None: sa.Column("description", sa.Text(), nullable=True), sa.Column( "thread_kind", - sa.Enum("build", "research", "decision", "admin", "recovery", + ENUM("build", "research", "decision", "admin", "recovery", name="thread_kind", schema="core", create_type=False), nullable=False, ), sa.Column( "status", - sa.Enum("active", "blocked", "paused", "closed", + ENUM("active", "blocked", "paused", "closed", name="thread_status", schema="core", create_type=False), nullable=False, ), @@ -163,7 +163,7 @@ def upgrade() -> None: sa.Column("description", sa.Text(), nullable=False), sa.Column( "hardness", - sa.Enum("hard", "soft", "assumed", + ENUM("hard", "soft", "assumed", name="constraint_hardness", schema="core", create_type=False), nullable=False, ), diff --git a/migrations/versions/006_execution_tables.py b/migrations/versions/006_execution_tables.py index 9287669..f3fb8f1 100644 --- a/migrations/versions/006_execution_tables.py +++ b/migrations/versions/006_execution_tables.py @@ -18,7 +18,7 @@ from alembic import op import sqlalchemy as sa -from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.dialects.postgresql import ENUM, JSONB revision: str = "006" down_revision: Union[str, None] = "005" @@ -35,7 +35,7 @@ def upgrade() -> None: sa.Column("thread_id", sa.Uuid(), sa.ForeignKey("vector_ctrl.threads.id"), nullable=True), sa.Column( "state_kind", - sa.Enum( + ENUM( "recover", "preserve", "orient", "clarify", "reenter", "execute", "deep_work", "close_loop", "review", "drift", name="state_kind", schema="core", create_type=False, @@ -66,7 +66,7 @@ def upgrade() -> None: sa.Column("thread_id", sa.Uuid(), sa.ForeignKey("vector_ctrl.threads.id"), nullable=True), sa.Column( "blocker_kind", - sa.Enum( + ENUM( "ambiguity", "scope_overload", "physiological_depletion", "emotional_threat", "context_fracture", "vector_conflict", "environmental_friction", "stimulation_hijack", "narrative_distortion", @@ -138,7 +138,7 @@ def upgrade() -> None: sa.Column("blocker_estimate_id", sa.Uuid(), sa.ForeignKey("execution.blocker_estimates.id"), nullable=True), sa.Column( "mode", - sa.Enum( + ENUM( "recover", "preserve", "orient", "reenter", "clarify", "ask", "execute", "close_loop", "review", name="intervention_kind", schema="core", create_type=False, @@ -147,7 +147,7 @@ def upgrade() -> None: ), sa.Column( "intervention_kind", - sa.Enum( + ENUM( "recover", "preserve", "orient", "reenter", "clarify", "ask", "execute", "close_loop", "review", name="intervention_kind", schema="core", create_type=False, @@ -156,7 +156,7 @@ def upgrade() -> None: ), sa.Column( "action_depth", - sa.Enum( + ENUM( "tiny", "narrow", "moderate", "deep", name="action_depth", schema="core", create_type=False, ), @@ -189,7 +189,7 @@ def upgrade() -> None: sa.Column("policy_decision_id", sa.Uuid(), sa.ForeignKey("execution.policy_decisions.id"), nullable=True), sa.Column( "trace_kind", - sa.Enum( + ENUM( "action", "question", "preserve", "recovery", "closure", name="trace_kind", schema="core", create_type=False, ), diff --git a/migrations/versions/010_review_tables.py b/migrations/versions/010_review_tables.py index 65e5bc1..79de72c 100644 --- a/migrations/versions/010_review_tables.py +++ b/migrations/versions/010_review_tables.py @@ -16,7 +16,7 @@ from alembic import op import sqlalchemy as sa -from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.dialects.postgresql import ENUM, JSONB revision: str = "010" down_revision: Union[str, None] = "009" @@ -35,7 +35,7 @@ def upgrade() -> None: sa.Column("operator_id", sa.Uuid(), sa.ForeignKey("core.operators.id"), nullable=False), sa.Column("thread_id", sa.Uuid(), sa.ForeignKey("vector_ctrl.threads.id"), nullable=True), sa.Column("mission_id", sa.Uuid(), sa.ForeignKey("vector_ctrl.missions.id"), nullable=True), - sa.Column("closure_type", sa.Enum("complete", "archive", "pause", "merge", name="closure_type", schema="core", create_type=False), nullable=False), + sa.Column("closure_type", ENUM("complete", "archive", "pause", "merge", name="closure_type", schema="core", create_type=False), nullable=False), sa.Column("outcome_summary", sa.Text(), nullable=True), sa.Column("lessons_learned", JSONB(), nullable=False, server_default="[]"), sa.Column("truth_revealed", sa.Text(), nullable=True), @@ -65,7 +65,7 @@ def upgrade() -> None: "review_sessions", sa.Column("id", sa.Uuid(), primary_key=True), sa.Column("operator_id", sa.Uuid(), sa.ForeignKey("core.operators.id"), nullable=False), - sa.Column("review_scope", sa.Enum("micro", "daily", "weekly", "monthly", name="review_scope", schema="core", create_type=False), nullable=False), + sa.Column("review_scope", ENUM("micro", "daily", "weekly", "monthly", name="review_scope", schema="core", create_type=False), nullable=False), sa.Column("window_start", sa.DateTime(timezone=True), nullable=False), sa.Column("window_end", sa.DateTime(timezone=True), nullable=False), sa.Column("summary", sa.Text(), nullable=True), diff --git a/migrations/versions/011_system_audit_tables.py b/migrations/versions/011_system_audit_tables.py index cafc3fe..4e2c9a8 100644 --- a/migrations/versions/011_system_audit_tables.py +++ b/migrations/versions/011_system_audit_tables.py @@ -15,7 +15,7 @@ from alembic import op import sqlalchemy as sa -from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.dialects.postgresql import ENUM, JSONB revision: str = "011" down_revision: Union[str, None] = "010" @@ -152,7 +152,7 @@ def upgrade() -> None: sa.Column("event_type", sa.Text(), nullable=False), sa.Column("destination", sa.Text(), nullable=False), sa.Column("payload", JSONB(), nullable=False, server_default="{}"), - sa.Column("status", sa.Enum("pending", "processed", "failed", name="processing_status", schema="core", create_type=False), nullable=False), + sa.Column("status", ENUM("pending", "processed", "failed", name="processing_status", schema="core", create_type=False), nullable=False), sa.Column("attempts", sa.Integer(), nullable=False, server_default="0"), sa.Column("last_error", sa.Text(), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")), diff --git a/migrations/versions/014_operator_scoped_embeddings.py b/migrations/versions/014_operator_scoped_embeddings.py new file mode 100644 index 0000000..d13c6ca --- /dev/null +++ b/migrations/versions/014_operator_scoped_embeddings.py @@ -0,0 +1,43 @@ +"""M014: scope vector embedding identity by operator. + +Revision ID: 014 +Revises: 013 +""" +from collections.abc import Sequence + +from alembic import op + +revision: str = "014" +down_revision: str | None = "013" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.drop_index( + "ix_vector_embeddings_entity", + table_name="embeddings", + schema="vector", + ) + op.create_index( + "ix_vector_embeddings_entity", + "embeddings", + ["operator_id", "entity_kind", "entity_id"], + unique=True, + schema="vector", + ) + + +def downgrade() -> None: + op.drop_index( + "ix_vector_embeddings_entity", + table_name="embeddings", + schema="vector", + ) + op.create_index( + "ix_vector_embeddings_entity", + "embeddings", + ["entity_kind", "entity_id"], + unique=True, + schema="vector", + ) diff --git a/pyproject.toml b/pyproject.toml index 3b15ad3..aae63b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "utopia" version = "0.1.0" -description = "Private cognitive operating system and judgment refinery" +description = "Historical multimodal neurocognitive state, continuity, and adaptive-control architecture" requires-python = ">=3.12" dependencies = [ "fastapi>=0.115.0", @@ -9,6 +9,7 @@ dependencies = [ "sqlalchemy[asyncio]>=2.0.36", "alembic>=1.14.0", "asyncpg>=0.30.0", + "psycopg[binary]>=3.2.0", "pgvector>=0.3.6", "pydantic>=2.10.0", "pydantic-settings>=2.6.0", diff --git a/src/utopia/ai/assess.py b/src/utopia/ai/assess.py index e4a8672..415a8a5 100644 --- a/src/utopia/ai/assess.py +++ b/src/utopia/ai/assess.py @@ -15,11 +15,6 @@ from typing import Any from utopia.ai import blocker_classifier, policy_selector, state_estimator -from utopia.schemas.execution import ( - BlockerEstimateCreate, - PolicyDecisionCreate, - StateEstimateCreate, -) from utopia.services.evidence_service import EvidenceService from utopia.services.execution_service import ExecutionService @@ -98,9 +93,9 @@ async def gather_evidence( evidence["derived_features"] = [ { "feature_name": f.feature_name, - "feature_value": str(f.feature_value) if f.feature_value else None, + "feature_value": str(f.feature_value) if f.feature_value is not None else None, "feature_window": f.feature_window, - "confidence": str(f.confidence) if f.confidence else None, + "confidence": str(f.confidence) if f.confidence is not None else None, } for f in features ] diff --git a/src/utopia/ai/context_retriever.py b/src/utopia/ai/context_retriever.py index 0324442..af9f5fc 100644 --- a/src/utopia/ai/context_retriever.py +++ b/src/utopia/ai/context_retriever.py @@ -65,7 +65,7 @@ async def retrieve_context( vector_search_svc: VectorSearchService, query: str, *, - operator_id: uuid.UUID | None = None, + operator_id: uuid.UUID, entity_kinds: list[str] | None = None, top_k: int = 10, synthesize: bool = True, @@ -75,7 +75,7 @@ async def retrieve_context( Args: vector_search_svc: The vector search service for semantic retrieval. query: Natural language query to search for. - operator_id: Optional filter by operator. + operator_id: Operator whose memory may be retrieved. entity_kinds: Optional filter to specific Aether entity types. top_k: Number of raw results to retrieve. synthesize: Whether to run Claude synthesis on results (set False for raw search). diff --git a/src/utopia/ai/council.py b/src/utopia/ai/council.py index f13ec50..70cb090 100644 --- a/src/utopia/ai/council.py +++ b/src/utopia/ai/council.py @@ -11,7 +11,6 @@ import json import logging -import uuid from dataclasses import dataclass, field from utopia.ai.providers import claude diff --git a/src/utopia/api/app.py b/src/utopia/api/app.py index 8dc51ee..1a05316 100644 --- a/src/utopia/api/app.py +++ b/src/utopia/api/app.py @@ -1,7 +1,4 @@ -"""Utopia FastAPI application. - -Minimal bootstrap — routes added per bounded context as they are built. -""" +"""Historical Neurocognitive System (Utopia) FastAPI application.""" from fastapi import FastAPI @@ -17,8 +14,11 @@ from utopia.api.routes.vector_search import router as vector_search_router app = FastAPI( - title="Utopia", - description="Private cognitive operating system and judgment refinery", + title="Neurocognitive System", + description=( + "Historical Utopia prototype: multimodal cognitive-state, continuity, " + "reasoning, and adaptive-control architecture. Not production hardened." + ), version="0.1.0", ) @@ -36,4 +36,8 @@ @app.get("/health") async def health() -> dict: - return {"status": "ok"} + return { + "status": "ok", + "system": "neurocognitive-system", + "historical_codename": "utopia", + } diff --git a/src/utopia/api/routes/aether.py b/src/utopia/api/routes/aether.py index bc19b1c..b1a1112 100644 --- a/src/utopia/api/routes/aether.py +++ b/src/utopia/api/routes/aether.py @@ -75,6 +75,8 @@ async def add_source_chunk( data: SourceChunkCreate, svc: AetherService = Depends(get_aether_service), ) -> SourceChunkRead: + if data.source_id != source_id: + raise HTTPException(status_code=422, detail="source_id must match the path") chunk = await svc.add_source_chunk(data) await svc.commit() return SourceChunkRead.model_validate(chunk) @@ -86,6 +88,8 @@ async def record_extraction( data: ExtractionCreate, svc: AetherService = Depends(get_aether_service), ) -> ExtractionRead: + if data.source_id != source_id: + raise HTTPException(status_code=422, detail="source_id must match the path") extraction = await svc.record_extraction(data) await svc.commit() return ExtractionRead.model_validate(extraction) @@ -229,6 +233,8 @@ async def add_lens_pack_item( data: LensPackItemCreate, svc: AetherService = Depends(get_aether_service), ) -> LensPackItemRead: + if data.lens_pack_id != lens_pack_id: + raise HTTPException(status_code=422, detail="lens_pack_id must match the path") item = await svc.add_lens_pack_item(data) await svc.commit() return LensPackItemRead.model_validate(item) diff --git a/src/utopia/api/routes/ai.py b/src/utopia/api/routes/ai.py index c407d28..c8a5c6e 100644 --- a/src/utopia/api/routes/ai.py +++ b/src/utopia/api/routes/ai.py @@ -173,7 +173,7 @@ async def structure_problem( class RetrieveRequest(BaseModel): query: str - operator_id: uuid.UUID | None = None + operator_id: uuid.UUID entity_kinds: list[str] | None = None top_k: int = 10 synthesize: bool = True diff --git a/src/utopia/api/routes/reasoning.py b/src/utopia/api/routes/reasoning.py index 19c3308..ea7b5ef 100644 --- a/src/utopia/api/routes/reasoning.py +++ b/src/utopia/api/routes/reasoning.py @@ -68,6 +68,8 @@ async def create_problem_structure( data: ProblemStructureCreate, svc: ReasoningService = Depends(get_reasoning_service), ) -> ProblemStructureRead: + if data.problem_id != problem_id: + raise HTTPException(status_code=422, detail="problem_id must match the path") structure = await svc.create_problem_structure(data) await svc.commit() return ProblemStructureRead.model_validate(structure) @@ -94,6 +96,8 @@ async def create_interrogation( data: InterrogationCreate, svc: ReasoningService = Depends(get_reasoning_service), ) -> InterrogationRead: + if data.problem_id != problem_id: + raise HTTPException(status_code=422, detail="problem_id must match the path") interrogation = await svc.create_interrogation(data) await svc.commit() return InterrogationRead.model_validate(interrogation) @@ -118,6 +122,8 @@ async def create_decision_brief( data: DecisionBriefCreate, svc: ReasoningService = Depends(get_reasoning_service), ) -> DecisionBriefRead: + if data.problem_id != problem_id: + raise HTTPException(status_code=422, detail="problem_id must match the path") brief = await svc.create_decision_brief(data) await svc.commit() return DecisionBriefRead.model_validate(brief) @@ -144,6 +150,8 @@ async def add_option_path( data: OptionPathCreate, svc: ReasoningService = Depends(get_reasoning_service), ) -> OptionPathRead: + if data.decision_brief_id != brief_id: + raise HTTPException(status_code=422, detail="decision_brief_id must match the path") option = await svc.add_option_path(data) await svc.commit() return OptionPathRead.model_validate(option) diff --git a/src/utopia/api/routes/review.py b/src/utopia/api/routes/review.py index fc7b3a0..8e99eb4 100644 --- a/src/utopia/api/routes/review.py +++ b/src/utopia/api/routes/review.py @@ -108,6 +108,8 @@ async def record_rule_promotion( data: RulePromotionCreate, svc: ReviewService = Depends(get_review_service), ) -> RulePromotionRead: + if data.review_session_id != session_id: + raise HTTPException(status_code=422, detail="review_session_id must match the path") promotion = await svc.record_rule_promotion(data) await svc.commit() return RulePromotionRead.model_validate(promotion) @@ -139,6 +141,8 @@ async def record_pattern_update( data: PatternUpdateCreate, svc: ReviewService = Depends(get_review_service), ) -> PatternUpdateRead: + if data.review_session_id != session_id: + raise HTTPException(status_code=422, detail="review_session_id must match the path") update = await svc.record_pattern_update(data) await svc.commit() return PatternUpdateRead.model_validate(update) diff --git a/src/utopia/api/routes/vector_search.py b/src/utopia/api/routes/vector_search.py index 942c07d..5d5904e 100644 --- a/src/utopia/api/routes/vector_search.py +++ b/src/utopia/api/routes/vector_search.py @@ -1,9 +1,8 @@ """Vector search routes — embedding management and semantic retrieval.""" import uuid -from typing import Any -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends from pydantic import BaseModel, Field from utopia.api.deps import get_vector_search_service @@ -39,7 +38,7 @@ class EmbeddingRead(BaseModel): class SearchRequest(BaseModel): query: str - operator_id: uuid.UUID | None = None + operator_id: uuid.UUID entity_kinds: list[str] | None = None top_k: int = Field(default=10, ge=1, le=100) @@ -52,6 +51,7 @@ class SearchResultRead(BaseModel): class SimilarRequest(BaseModel): + operator_id: uuid.UUID entity_kind: str entity_id: uuid.UUID top_k: int = Field(default=5, ge=1, le=50) @@ -144,7 +144,7 @@ async def find_similar( ) -> list[SearchResultRead]: """Find entities similar to a given entity.""" results = await svc.find_similar( - data.entity_kind, data.entity_id, top_k=data.top_k + data.operator_id, data.entity_kind, data.entity_id, top_k=data.top_k ) return [ SearchResultRead( diff --git a/src/utopia/config.py b/src/utopia/config.py index 15c3c56..09bb70b 100644 --- a/src/utopia/config.py +++ b/src/utopia/config.py @@ -1,4 +1,4 @@ -"""Utopia configuration loaded from environment variables.""" +"""Historical Utopia runtime configuration.""" from pydantic_settings import BaseSettings @@ -6,9 +6,9 @@ class Settings(BaseSettings): model_config = {"env_file": ".env", "env_file_encoding": "utf-8"} - # PostgreSQL (async driver for app, sync driver for alembic) + # PostgreSQL: async application driver + synchronous Alembic driver. database_url: str = "postgresql+asyncpg://utopia:utopia@localhost:5432/utopia" - database_url_sync: str = "postgresql+psycopg2://utopia:utopia@localhost:5432/utopia" + database_url_sync: str = "postgresql+psycopg://utopia:utopia@localhost:5432/utopia" # WHOOP integration whoop_client_id: str = "" diff --git a/src/utopia/ids.py b/src/utopia/ids.py new file mode 100644 index 0000000..9dd6292 --- /dev/null +++ b/src/utopia/ids.py @@ -0,0 +1,16 @@ +"""UUID helpers for the historical Utopia implementation. + +The project originally used uuid-utils for UUIDv7 generation while the ORM +and Pydantic schemas were typed against the standard-library uuid.UUID. +Normalize at generation time so UUIDv7 semantics are preserved without +leaking a second UUID runtime type through service boundaries. +""" + +import uuid + +from uuid_utils import uuid7 as _uuid7 + + +def new_uuid7() -> uuid.UUID: + """Generate a UUIDv7 represented as a standard-library UUID.""" + return uuid.UUID(str(_uuid7())) diff --git a/src/utopia/schemas/evidence.py b/src/utopia/schemas/evidence.py index 45d5f40..31c293b 100644 --- a/src/utopia/schemas/evidence.py +++ b/src/utopia/schemas/evidence.py @@ -32,7 +32,9 @@ class SubjectiveCheckinCreate(BaseModel): emotional_load: int | None = Field(None, ge=0, le=100) perceived_urgency: int | None = Field(None, ge=0, le=100) free_text: str | None = None - recorded_at: datetime.datetime + recorded_at: datetime.datetime = Field( + default_factory=lambda: datetime.datetime.now(datetime.timezone.utc) + ) class SubjectiveCheckinRead(BaseModel): @@ -136,7 +138,9 @@ class DerivedFeatureCreate(BaseModel): feature_json: dict | None = None feature_window: str | None = None confidence: Decimal | None = Field(None, ge=0, le=1) - observed_at: datetime.datetime + observed_at: datetime.datetime = Field( + default_factory=lambda: datetime.datetime.now(datetime.timezone.utc) + ) class DerivedFeatureRead(BaseModel): diff --git a/src/utopia/services/aether_service.py b/src/utopia/services/aether_service.py index d5b471e..a1f49fe 100644 --- a/src/utopia/services/aether_service.py +++ b/src/utopia/services/aether_service.py @@ -13,7 +13,7 @@ from sqlalchemy import select from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.ext.asyncio import AsyncSession -from uuid_utils import uuid7 +from utopia.ids import new_uuid7 from utopia.models.aether import ( Case, @@ -72,7 +72,7 @@ async def commit(self) -> None: async def ingest_source(self, data: SourceCreate) -> Source: source = Source( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, source_kind=data.source_kind, title=data.title, @@ -93,7 +93,7 @@ async def get_source(self, source_id: _uuid.UUID) -> Source | None: async def add_source_chunk(self, data: SourceChunkCreate) -> SourceChunk: chunk = SourceChunk( - id=uuid7(), + id=new_uuid7(), source_id=data.source_id, chunk_index=data.chunk_index, raw_text=data.raw_text, @@ -110,7 +110,7 @@ async def record_extraction(self, data: ExtractionCreate) -> Extraction: stmt = ( pg_insert(Extraction) .values( - id=uuid7(), + id=new_uuid7(), source_id=data.source_id, extraction_version=data.extraction_version, extraction_status=data.extraction_status, @@ -146,7 +146,7 @@ async def create_concept(self, data: ConceptCreate) -> Concept: stmt = ( pg_insert(Concept) .values( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, canonical_name=data.canonical_name, definition=data.definition, @@ -182,7 +182,7 @@ async def list_concepts( async def create_mechanism(self, data: MechanismCreate) -> Mechanism: mechanism = Mechanism( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, name=data.name, description=data.description, @@ -196,7 +196,7 @@ async def create_mechanism(self, data: MechanismCreate) -> Mechanism: async def create_tradeoff(self, data: TradeoffCreate) -> Tradeoff: tradeoff = Tradeoff( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, name=data.name, pole_a=data.pole_a, @@ -210,7 +210,7 @@ async def create_tradeoff(self, data: TradeoffCreate) -> Tradeoff: async def create_failure_mode(self, data: FailureModeCreate) -> FailureMode: failure_mode = FailureMode( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, name=data.name, description=data.description, @@ -224,7 +224,7 @@ async def create_failure_mode(self, data: FailureModeCreate) -> FailureMode: async def create_heuristic(self, data: HeuristicCreate) -> Heuristic: heuristic = Heuristic( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, statement=data.statement, domain=data.domain, @@ -240,7 +240,7 @@ async def create_diagnostic_question( self, data: DiagnosticQuestionCreate ) -> DiagnosticQuestion: question = DiagnosticQuestion( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, question_text=data.question_text, question_class=data.question_class, @@ -253,7 +253,7 @@ async def create_diagnostic_question( async def create_protocol(self, data: ProtocolCreate) -> Protocol: protocol = Protocol( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, protocol_name=data.protocol_name, domain=data.domain, @@ -271,7 +271,7 @@ async def create_protocol(self, data: ProtocolCreate) -> Protocol: async def create_lens_pack(self, data: LensPackCreate) -> LensPack: pack = LensPack( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, name=data.name, domain=data.domain, @@ -288,7 +288,7 @@ async def get_lens_pack(self, lens_pack_id: _uuid.UUID) -> LensPack | None: async def add_lens_pack_item(self, data: LensPackItemCreate) -> LensPackItem: item = LensPackItem( - id=uuid7(), + id=new_uuid7(), lens_pack_id=data.lens_pack_id, item_kind=data.item_kind, item_id=data.item_id, @@ -315,7 +315,7 @@ async def list_lens_pack_items( async def create_case(self, data: CaseCreate) -> Case: case = Case( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, title=data.title, case_kind=data.case_kind, @@ -333,7 +333,7 @@ async def create_case(self, data: CaseCreate) -> Case: async def create_rule(self, data: RuleCreate) -> Rule: rule = Rule( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, rule_text=data.rule_text, rule_kind=data.rule_kind, @@ -381,7 +381,7 @@ async def list_rules( async def create_pattern(self, data: PatternCreate) -> Pattern: pattern = Pattern( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, pattern_name=data.pattern_name, description=data.description, @@ -420,7 +420,7 @@ async def list_patterns( async def create_edge(self, data: EdgeCreate) -> Edge: edge = Edge( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, src_kind=data.src_kind, dst_kind=data.dst_kind, diff --git a/src/utopia/services/evidence_service.py b/src/utopia/services/evidence_service.py index e6fc77d..edd88df 100644 --- a/src/utopia/services/evidence_service.py +++ b/src/utopia/services/evidence_service.py @@ -10,7 +10,7 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from uuid_utils import uuid7 +from utopia.ids import new_uuid7 from utopia.models.evidence import ( BehaviorEvent, @@ -50,7 +50,7 @@ async def record_checkin(self, data: SubjectiveCheckinCreate) -> SubjectiveCheck is meaningful evidence for the State Estimator. """ checkin = SubjectiveCheckin( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, thread_id=data.thread_id, energy=data.energy, @@ -98,7 +98,7 @@ async def record_behavior_event(self, data: BehaviorEventCreate) -> BehaviorEven aversion. The Blocker Classifier consumes these. """ event = BehaviorEvent( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, thread_id=data.thread_id, event_type=data.event_type, @@ -142,7 +142,7 @@ async def record_context_snapshot(self, data: ContextSnapshotCreate) -> ContextS the context it needs to weight feasibility of action depth. """ snapshot = ContextSnapshot( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, thread_id=data.thread_id, local_time=data.local_time, @@ -182,7 +182,7 @@ async def store_derived_feature(self, data: DerivedFeatureCreate) -> DerivedFeat thread_decay_hours, drift_probability, completion_aversion_score. """ feature = DerivedFeature( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, thread_id=data.thread_id, feature_name=data.feature_name, diff --git a/src/utopia/services/execution_service.py b/src/utopia/services/execution_service.py index 86ece44..6ef0ef7 100644 --- a/src/utopia/services/execution_service.py +++ b/src/utopia/services/execution_service.py @@ -15,7 +15,7 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from uuid_utils import uuid7 +from utopia.ids import new_uuid7 from utopia.models.execution import ( BlockerEstimate, @@ -58,7 +58,7 @@ async def record_state_estimate(self, data: StateEstimateCreate) -> StateEstimat downstream policy selection. """ estimate = StateEstimate( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, thread_id=data.thread_id, state_kind=data.state_kind, @@ -96,7 +96,7 @@ async def record_blocker_estimate(self, data: BlockerEstimateCreate) -> BlockerE blocker typing, the system recommends the wrong thing. """ estimate = BlockerEstimate( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, thread_id=data.thread_id, blocker_kind=data.blocker_kind, @@ -134,7 +134,7 @@ async def create_reentry_artifact(self, data: ReentryArtifactCreate) -> ReentryA unresolved edge, smallest next move, trap to avoid. """ artifact = ReentryArtifact( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, thread_id=data.thread_id, last_completed_step=data.last_completed_step, @@ -193,7 +193,7 @@ async def record_policy_decision(self, data: PolicyDecisionCreate) -> PolicyDeci AI Fabric exists, this can be created manually or by deterministic rules. """ decision = PolicyDecision( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, thread_id=data.thread_id, problem_id=data.problem_id, @@ -237,7 +237,7 @@ async def record_trace(self, data: TraceCreate) -> Trace: Model. Completes the chain: evidence -> inference -> policy -> outcome. """ trace = Trace( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, thread_id=data.thread_id, policy_decision_id=data.policy_decision_id, diff --git a/src/utopia/services/physiology_service.py b/src/utopia/services/physiology_service.py index 477cf7f..8f0cc72 100644 --- a/src/utopia/services/physiology_service.py +++ b/src/utopia/services/physiology_service.py @@ -14,7 +14,7 @@ from sqlalchemy import select from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.ext.asyncio import AsyncSession -from uuid_utils import uuid7 +from utopia.ids import new_uuid7 from utopia.models.physiology import ( BiomarkerPanel, @@ -58,7 +58,7 @@ async def commit(self) -> None: async def connect_whoop(self, data: WhoopConnectionCreate) -> WhoopConnection: conn = WhoopConnection( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, oauth_connection_id=data.oauth_connection_id, scope_granted=data.scope_granted, @@ -79,7 +79,7 @@ async def record_body_measurement( self, data: WhoopBodyMeasurementCreate ) -> WhoopBodyMeasurement: measurement = WhoopBodyMeasurement( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, measured_at=data.measured_at, height_cm=data.height_cm, @@ -100,7 +100,7 @@ async def upsert_cycle(self, data: WhoopCycleCreate) -> WhoopCycle: stmt = ( pg_insert(WhoopCycle) .values( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, provider_cycle_id=data.provider_cycle_id, whoop_user_id=data.whoop_user_id, @@ -141,7 +141,7 @@ async def upsert_sleep(self, data: WhoopSleepCreate) -> WhoopSleep: stmt = ( pg_insert(WhoopSleep) .values( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, provider_sleep_id=data.provider_sleep_id, provider_cycle_id=data.provider_cycle_id, @@ -185,7 +185,7 @@ async def upsert_recovery(self, data: WhoopRecoveryCreate) -> WhoopRecovery: stmt = ( pg_insert(WhoopRecovery) .values( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, provider_cycle_id=data.provider_cycle_id, provider_sleep_id=data.provider_sleep_id, @@ -230,7 +230,7 @@ async def upsert_workout(self, data: WhoopWorkoutCreate) -> WhoopWorkout: stmt = ( pg_insert(WhoopWorkout) .values( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, provider_workout_id=data.provider_workout_id, workout_type=data.workout_type, @@ -268,7 +268,7 @@ async def store_physiology_feature( stmt = ( pg_insert(PhysiologyFeature) .values( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, feature_date=data.feature_date, feature_name=data.feature_name, @@ -317,7 +317,7 @@ async def record_biomarker_panel( self, data: BiomarkerPanelCreate ) -> BiomarkerPanel: panel = BiomarkerPanel( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, panel_date=data.panel_date, provider=data.provider, diff --git a/src/utopia/services/reasoning_service.py b/src/utopia/services/reasoning_service.py index 212000f..686e074 100644 --- a/src/utopia/services/reasoning_service.py +++ b/src/utopia/services/reasoning_service.py @@ -13,7 +13,7 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from uuid_utils import uuid7 +from utopia.ids import new_uuid7 from utopia.models.reasoning import ( ContradictionReport, @@ -52,7 +52,7 @@ async def commit(self) -> None: async def create_problem(self, data: ProblemCreate) -> Problem: problem = Problem( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, title=data.title, raw_prompt=data.raw_prompt, @@ -92,7 +92,7 @@ async def create_problem_structure( self, data: ProblemStructureCreate ) -> ProblemStructure: structure = ProblemStructure( - id=uuid7(), + id=new_uuid7(), problem_id=data.problem_id, objective=data.objective, stakes=data.stakes, @@ -130,7 +130,7 @@ async def get_problem_structure( async def create_interrogation(self, data: InterrogationCreate) -> Interrogation: interrogation = Interrogation( - id=uuid7(), + id=new_uuid7(), problem_id=data.problem_id, interrogation_kind=data.interrogation_kind, questions=data.questions, @@ -157,7 +157,7 @@ async def list_interrogations( async def create_decision_brief(self, data: DecisionBriefCreate) -> DecisionBrief: brief = DecisionBrief( - id=uuid7(), + id=new_uuid7(), problem_id=data.problem_id, classification=data.classification, summary=data.summary, @@ -190,7 +190,7 @@ async def get_decision_brief( async def add_option_path(self, data: OptionPathCreate) -> OptionPath: option = OptionPath( - id=uuid7(), + id=new_uuid7(), decision_brief_id=data.decision_brief_id, option_label=data.option_label, description=data.description, @@ -223,7 +223,7 @@ async def record_contradiction( self, data: ContradictionReportCreate ) -> ContradictionReport: report = ContradictionReport( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, problem_id=data.problem_id, contradiction_kind=data.contradiction_kind, diff --git a/src/utopia/services/review_service.py b/src/utopia/services/review_service.py index 2ce9ee0..f2eef11 100644 --- a/src/utopia/services/review_service.py +++ b/src/utopia/services/review_service.py @@ -12,7 +12,7 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from uuid_utils import uuid7 +from utopia.ids import new_uuid7 from utopia.enums import ReviewScope from utopia.models.review import ( @@ -50,7 +50,7 @@ async def commit(self) -> None: async def create_closure(self, data: ClosureCreate) -> Closure: closure = Closure( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, thread_id=data.thread_id, mission_id=data.mission_id, @@ -95,7 +95,7 @@ async def create_review_session( self, data: ReviewSessionCreate ) -> ReviewSession: session = ReviewSession( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, review_scope=data.review_scope, window_start=data.window_start, @@ -138,7 +138,7 @@ async def record_rule_promotion( self, data: RulePromotionCreate ) -> RulePromotion: promotion = RulePromotion( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, review_session_id=data.review_session_id, rule_id=data.rule_id, @@ -174,7 +174,7 @@ async def record_pattern_update( self, data: PatternUpdateCreate ) -> PatternUpdate: update = PatternUpdate( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, review_session_id=data.review_session_id, pattern_id=data.pattern_id, @@ -212,7 +212,7 @@ async def record_calibration( self, data: CalibrationRecordCreate ) -> CalibrationRecord: record = CalibrationRecord( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, review_session_id=data.review_session_id, estimate_kind=data.estimate_kind, diff --git a/src/utopia/services/system_audit_service.py b/src/utopia/services/system_audit_service.py index 66e3b0f..9bfc425 100644 --- a/src/utopia/services/system_audit_service.py +++ b/src/utopia/services/system_audit_service.py @@ -9,7 +9,7 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from uuid_utils import uuid7 +from utopia.ids import new_uuid7 from utopia.enums import ProcessingStatus from utopia.models.system_audit import ( @@ -49,7 +49,7 @@ async def create_model_provider( self, data: ModelProviderCreate ) -> ModelProvider: provider = ModelProvider( - id=uuid7(), + id=new_uuid7(), name=data.name, provider_kind=data.provider_kind, api_base_url=data.api_base_url, @@ -82,7 +82,7 @@ async def list_model_providers( async def record_model_run(self, data: ModelRunCreate) -> ModelRun: run = ModelRun( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, provider_id=data.provider_id, model_name=data.model_name, @@ -127,7 +127,7 @@ async def record_retrieval_run( self, data: RetrievalRunCreate ) -> RetrievalRun: run = RetrievalRun( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, model_run_id=data.model_run_id, collection=data.collection, @@ -170,7 +170,7 @@ async def list_retrieval_runs( async def log_event(self, data: EventLogCreate) -> EventLog: event = EventLog( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, event_type=data.event_type, source=data.source, @@ -211,7 +211,7 @@ async def list_events( async def create_outbox_event(self, data: OutboxEventCreate) -> OutboxEvent: event = OutboxEvent( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, event_type=data.event_type, destination=data.destination, diff --git a/src/utopia/services/vector_search_service.py b/src/utopia/services/vector_search_service.py index a4ff20f..726c6e9 100644 --- a/src/utopia/services/vector_search_service.py +++ b/src/utopia/services/vector_search_service.py @@ -13,7 +13,7 @@ from sqlalchemy import delete, select, text from sqlalchemy.ext.asyncio import AsyncSession -from uuid_utils import uuid7 +from utopia.ids import new_uuid7 from utopia.ai.providers import openai_embeddings from utopia.config import settings @@ -79,6 +79,7 @@ async def embed_entity( # Check for existing embedding with same hash stmt = ( select(Embedding) + .where(Embedding.operator_id == operator_id) .where(Embedding.entity_kind == entity_kind) .where(Embedding.entity_id == entity_id) ) @@ -104,7 +105,7 @@ async def embed_entity( # Create new emb = Embedding( - id=uuid7(), + id=new_uuid7(), operator_id=operator_id, entity_kind=entity_kind, entity_id=entity_id, @@ -137,6 +138,7 @@ async def embed_entities_batch( content_hash = _hash_content(entity["content_text"]) stmt = ( select(Embedding) + .where(Embedding.operator_id == operator_id) .where(Embedding.entity_kind == entity["entity_kind"]) .where(Embedding.entity_id == entity["entity_id"]) ) @@ -167,7 +169,7 @@ async def embed_entities_batch( results.append(existing) else: emb = Embedding( - id=uuid7(), + id=new_uuid7(), operator_id=operator_id, entity_kind=entity_data["entity_kind"], entity_id=entity_data["entity_id"], @@ -187,11 +189,12 @@ async def embed_entities_batch( return results async def delete_embedding( - self, entity_kind: str, entity_id: _uuid.UUID + self, operator_id: _uuid.UUID, entity_kind: str, entity_id: _uuid.UUID ) -> None: - """Delete an embedding for an entity.""" + """Delete an embedding owned by an operator.""" stmt = ( delete(Embedding) + .where(Embedding.operator_id == operator_id) .where(Embedding.entity_kind == entity_kind) .where(Embedding.entity_id == entity_id) ) @@ -205,7 +208,7 @@ async def search( self, query: str, *, - operator_id: _uuid.UUID | None = None, + operator_id: _uuid.UUID, entity_kinds: list[str] | None = None, top_k: int = 10, ) -> list[SearchResult]: @@ -213,7 +216,7 @@ async def search( Args: query: The natural language search query. - operator_id: Optional filter by operator. + operator_id: Operator that owns the searchable memory. entity_kinds: Optional filter to specific entity types. top_k: Number of results to return. @@ -226,12 +229,8 @@ async def search( # 1 - cosine_distance = cosine_similarity vector_literal = f"[{','.join(str(v) for v in query_vector)}]" - filters = [] - params: dict = {"top_k": top_k} - - if operator_id is not None: - filters.append("operator_id = :operator_id") - params["operator_id"] = str(operator_id) + filters = ["operator_id = :operator_id"] + params: dict = {"top_k": top_k, "operator_id": str(operator_id)} if entity_kinds: placeholders = ", ".join(f":ek_{i}" for i in range(len(entity_kinds))) @@ -268,6 +267,7 @@ async def search( async def find_similar( self, + operator_id: _uuid.UUID, entity_kind: str, entity_id: _uuid.UUID, *, @@ -281,6 +281,7 @@ async def find_similar( # Get the entity's embedding stmt = ( select(Embedding) + .where(Embedding.operator_id == operator_id) .where(Embedding.entity_kind == entity_kind) .where(Embedding.entity_id == entity_id) ) @@ -299,14 +300,20 @@ async def find_similar( content_text, 1 - (embedding <=> '{vector_literal}'::vector) AS similarity FROM vector.embeddings - WHERE NOT (entity_kind = :src_kind AND entity_id = :src_id) + WHERE operator_id = :operator_id + AND NOT (entity_kind = :src_kind AND entity_id = :src_id) ORDER BY embedding <=> '{vector_literal}'::vector LIMIT :top_k """ result = await self._session.execute( text(sql), - {"src_kind": entity_kind, "src_id": str(entity_id), "top_k": top_k}, + { + "operator_id": str(operator_id), + "src_kind": entity_kind, + "src_id": str(entity_id), + "top_k": top_k, + }, ) rows = result.fetchall() diff --git a/src/utopia/services/vector_service.py b/src/utopia/services/vector_service.py index 7804428..693c703 100644 --- a/src/utopia/services/vector_service.py +++ b/src/utopia/services/vector_service.py @@ -14,7 +14,7 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from uuid_utils import uuid7 +from utopia.ids import new_uuid7 from utopia.models.vector_ctrl import ( AntiGoal, @@ -53,7 +53,7 @@ async def commit(self) -> None: async def create_life_arc(self, data: LifeArcCreate) -> LifeArc: arc = LifeArc( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, title=data.title, description=data.description, @@ -76,7 +76,7 @@ async def get_life_arc(self, life_arc_id: _uuid.UUID) -> LifeArc | None: async def create_season(self, data: SeasonCreate) -> Season: season = Season( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, life_arc_id=data.life_arc_id, title=data.title, @@ -105,7 +105,7 @@ async def create_mission(self, data: MissionCreate) -> Mission: are the fields that give Vector its governance power. """ mission = Mission( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, season_id=data.season_id, title=data.title, @@ -146,7 +146,7 @@ async def create_thread(self, data: ThreadCreate) -> Thread: Blocker Classifier, Schrodinger — use for policy selection. """ thread = Thread( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, mission_id=data.mission_id, parent_thread_id=data.parent_thread_id, @@ -184,7 +184,7 @@ async def list_threads( async def add_thread_constraint(self, data: ThreadConstraintCreate) -> ThreadConstraint: constraint = ThreadConstraint( - id=uuid7(), + id=new_uuid7(), thread_id=data.thread_id, constraint_type=data.constraint_type, description=data.description, @@ -205,7 +205,7 @@ async def create_anti_goal(self, data: AntiGoalCreate) -> AntiGoal: to detect drift and block misaligned action proposals. """ anti_goal = AntiGoal( - id=uuid7(), + id=new_uuid7(), operator_id=data.operator_id, scope_type=data.scope_type, scope_id=data.scope_id, diff --git a/tests/conftest.py b/tests/conftest.py index 033697f..dd61326 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,21 +12,25 @@ import pytest from httpx import ASGITransport, AsyncClient +from sqlalchemy.engine import make_url from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from utopia.api.app import app from utopia.api.deps import get_db from utopia.config import settings -from utopia.db import Base # --------------------------------------------------------------------------- # Database fixtures # --------------------------------------------------------------------------- -# Use the same database URL but with a test-aware approach: -# In CI, point DATABASE_URL to a test database. -# Locally, this uses the configured database. TEST_DATABASE_URL = settings.database_url +TEST_DATABASE_NAME = make_url(TEST_DATABASE_URL).database or "" + +if not TEST_DATABASE_NAME.endswith("_test"): + raise RuntimeError( + "Refusing to run tests unless DATABASE_URL names a database ending in '_test'. " + "Set DATABASE_URL and DATABASE_URL_SYNC to an isolated test database." + ) @pytest.fixture(scope="session") diff --git a/tests/test_ai_modules.py b/tests/test_ai_modules.py index 7e6da9c..9ac9806 100644 --- a/tests/test_ai_modules.py +++ b/tests/test_ai_modules.py @@ -10,7 +10,6 @@ import json import uuid from decimal import Decimal -from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/test_routes.py b/tests/test_routes.py index 1a02830..facf8f2 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -9,7 +9,6 @@ import json import uuid -from unittest.mock import AsyncMock, patch import pytest @@ -37,7 +36,11 @@ class TestHealth: async def test_health(self, client): resp = await client.get("/health") assert resp.status_code == 200 - assert resp.json() == {"status": "ok"} + assert resp.json() == { + "status": "ok", + "system": "neurocognitive-system", + "historical_codename": "utopia", + } # --------------------------------------------------------------------------- @@ -327,3 +330,48 @@ async def _mock(*, system, user, max_tokens, **kwargs): data = resp.json() assert len(data["perspectives"]) == 4 assert data["decision_readiness"] == "defer" + + +# --------------------------------------------------------------------------- +# Nested resource integrity +# --------------------------------------------------------------------------- + +class TestNestedResourceIntegrity: + + @pytest.mark.asyncio + async def test_aether_source_path_must_match_body(self, client): + path_id = uuid.uuid4() + body_id = uuid.uuid4() + resp = await client.post( + f"/aether/sources/{path_id}/chunks", + json={ + "source_id": str(body_id), + "chunk_index": 0, + "raw_text": "test", + }, + ) + assert resp.status_code == 422 + + @pytest.mark.asyncio + async def test_reasoning_problem_path_must_match_body(self, client): + path_id = uuid.uuid4() + body_id = uuid.uuid4() + resp = await client.post( + f"/reasoning/problems/{path_id}/structure", + json={"problem_id": str(body_id)}, + ) + assert resp.status_code == 422 + + @pytest.mark.asyncio + async def test_review_session_path_must_match_body(self, client): + path_id = uuid.uuid4() + body_id = uuid.uuid4() + resp = await client.post( + f"/review/sessions/{path_id}/rule-promotions", + json={ + "operator_id": str(uuid.uuid4()), + "review_session_id": str(body_id), + "rule_id": str(uuid.uuid4()), + }, + ) + assert resp.status_code == 422 diff --git a/tests/test_services.py b/tests/test_services.py index 6f587cd..724085e 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -15,7 +15,7 @@ from decimal import Decimal import pytest -from uuid_utils import uuid7 +from utopia.ids import new_uuid7 from utopia.enums import ( ActionDepth, @@ -244,14 +244,14 @@ async def test_reentry_artifact_lifecycle(self, db_session, execution_service, o from utopia.models.vector_ctrl import Mission, Thread mission = Mission( - id=uuid7(), operator_id=operator_id, title="Test Mission", + id=new_uuid7(), operator_id=operator_id, title="Test Mission", mission_kind=MissionKind.technical, status=Status.active, ) db_session.add(mission) await db_session.flush() thread = Thread( - id=uuid7(), operator_id=operator_id, mission_id=mission.id, + id=new_uuid7(), operator_id=operator_id, mission_id=mission.id, title="Test Thread", thread_kind=ThreadKind.build, status=ThreadStatus.active, ) db_session.add(thread) @@ -351,3 +351,101 @@ async def test_contradiction_report(self, db_session, reasoning_service, operato reports = await reasoning_service.list_contradictions(operator_id) assert len(reports) >= 1 + + +# --------------------------------------------------------------------------- +# Cleanup regression coverage +# --------------------------------------------------------------------------- + +class TestCleanupRegressions: + """Regression tests for public-release correctness fixes.""" + + @pytest.mark.asyncio + async def test_zero_valued_derived_evidence_is_preserved( + self, db_session, evidence_service, operator_id + ): + from utopia.ai.assess import gather_evidence + + await _insert_operator(db_session, operator_id) + await evidence_service.store_derived_feature( + DerivedFeatureCreate( + operator_id=operator_id, + feature_name="zero_signal", + feature_value=Decimal("0"), + confidence=Decimal("0"), + ) + ) + + evidence = await gather_evidence(evidence_service, operator_id) + feature = evidence["derived_features"][0] + + assert Decimal(feature["feature_value"]) == 0 + assert Decimal(feature["confidence"]) == 0 + + @pytest.mark.asyncio + async def test_vector_retrieval_is_operator_scoped( + self, + db_session, + vector_search_service, + mock_embeddings, + operator_id, + ): + other_operator_id = uuid.uuid4() + shared_entity_id = uuid.uuid4() + + await _insert_operator(db_session, operator_id) + await _insert_operator(db_session, other_operator_id) + + await vector_search_service.embed_entity( + operator_id, + "concept", + shared_entity_id, + "operator one memory", + ) + await vector_search_service.embed_entity( + other_operator_id, + "concept", + shared_entity_id, + "operator two memory", + ) + + own_results = await vector_search_service.search( + "memory", + operator_id=operator_id, + top_k=10, + ) + + assert [result.content_text for result in own_results] == ["operator one memory"] + + @pytest.mark.asyncio + async def test_find_similar_is_operator_scoped( + self, + db_session, + vector_search_service, + mock_embeddings, + operator_id, + ): + other_operator_id = uuid.uuid4() + source_id = uuid.uuid4() + + await _insert_operator(db_session, operator_id) + await _insert_operator(db_session, other_operator_id) + + await vector_search_service.embed_entity( + operator_id, "concept", source_id, "source memory" + ) + await vector_search_service.embed_entity( + operator_id, "concept", uuid.uuid4(), "same operator neighbor" + ) + await vector_search_service.embed_entity( + other_operator_id, "concept", uuid.uuid4(), "other operator neighbor" + ) + + results = await vector_search_service.find_similar( + operator_id, + "concept", + source_id, + top_k=10, + ) + + assert [result.content_text for result in results] == ["same operator neighbor"]