feat(nouser): Phase D — proveedor Nous mock + cliente remoto

Cierra el patrón "Nous como módulo aparte intercambiable": el contrato
del proveedor de embeddings vive en su crate, el mock determinístico
implementa ese contrato sirviéndolo por Unix socket, y nouser-core
sabe consumirlo remotamente. El switch mock↔real (futuro) será vía
priority_contexts en el broker.

Crates nuevos:

- crates/modules/nouser/nous: contrato compartido.
  - EmbedRequest { kind: { EmbedFile | EmbedText | Ping }, payload }.
  - EmbedFilePayload (path, ext, size, mtime), EmbedTextPayload.
  - EmbedResponse (embedding, model, elapsed_ms), PingResponse,
    ErrorResponse.
  - Wire: line-delimited JSON sobre Unix socket, single-shot.
  - Constants FLOW_EMBED_REQUEST, FLOW_EMBED_RESULT, FLOW_TYPE_NAME.
  - transport::default_socket_path con env NOUSER_NOUS_SOCKET.

- crates/modules/nouser/nous-mock: bin nouser-nous-mock.
  - Sidecarea a brahman-init con Card kind=Ente declarando los flows
    embed-request/embed-result + priority_contexts.test = +1.
  - Bind del socket Nous + accept loop tokio.
  - EmbedFile delega a nouser_core::embed::embed (Phase C).
  - Modelo: "mock-pseudo-32d".

Cambios:

- nouser-core: dep nueva nouser-nous. Subcomando attract --remote
  abre un UnixStream blocking, envía EmbedRequest, lee response.
  Imprime "embed: local|remote" para ver cuál ruta corrió.

Bug encontrado y corregido:
- ContextBias tenía #[serde(skip_serializing_if = ...)] en sus campos.
  Postcard NO soporta skip-condicional en formatos no self-describing:
  el serializer omitía bytes que el deserializer esperaba, rompiendo
  la wire de cualquier Card con priority_contexts poblada.
  Síntoma: "postcard decode: Hit the end of buffer" en el server,
  "early eof" en el cliente.
- Fix: removidos los skip_serializing_if de ContextBias. JSON pretty
  ahora emite {"pin_to": null, "priority_offset": 0} pero el wire
  funciona. Trade-off aceptado.
- Test wirecard_postcard_with_priority_contexts en brahman-card que
  ejercita el roundtrip postcard con biases poblados.

Validación end-to-end:
  $ ente-zero & nouser-nous-mock & nouser daemon crates/core
  $ brahman-status
  Sessions (7):
    [ente] nouser.nous_mock      flows: embed-request, embed-result
    [ente] brahman.nouser_engine
    [data] src   summary: 6 archivos en crates/core/brahman-handshake/src
    [data] graph summary: 7 archivos en crates/core/ente-zero/src/graph
    ...
  $ nouser attract --remote crates/core <archivo>.rs
    embed: remote
    🧲  0.9058  src  ...
  (mock log: embed_file path=...)

Tests: 75. cargo check --workspace: 0 errores, 0 warnings.

Próximo natural: Phase D-2 — real-nous con ONNX/Llama text-embedding.
Declara la misma Card con priority_contexts.prod = +1 y el swap es
transparente para el consumer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-08 18:49:25 +00:00
parent 77faf12e82
commit b3c3c00cf2
10 changed files with 693 additions and 11 deletions
+74
View File
@@ -6,6 +6,80 @@ ratio/diff ver `git show <sha>`.
## 2026-05-08
### feat(nouser): Phase D — proveedor Nous mock + cliente remoto
Cierra el patrón "Nous como módulo aparte intercambiable": el contrato
del proveedor de embeddings vive en su crate, el mock determinístico
implementa ese contrato sirviéndolo por Unix socket, y `nouser-core`
sabe consumirlo remotamente. El switch entre mock y real (futuro) se
hará vía priority_contexts en el broker.
Crates nuevos:
- `crates/modules/nouser/nous`: contrato compartido. Tipos
`EmbedRequest`, `RequestKind { EmbedFile, EmbedText, Ping }`,
`EmbedFilePayload`, `EmbedTextPayload`, `EmbedResponse`,
`PingResponse`, `ErrorResponse`. Wire format: line-delimited JSON
por Unix socket, single-shot per conexión. Constants para los nombres
de flow (`embed-request`/`embed-result`) y el tipo (`json`). Helper
`transport::default_socket_path()` con env var
`NOUSER_NOUS_SOCKET`.
- `crates/modules/nouser/nous-mock`: bin `nouser-nous-mock`. Sidecarea
a brahman-init con Card kind=Ente declarando los flows
`embed-request:json`/`embed-result:json` y un
`priority_contexts.test = { priority_offset: +1 }` (gana sobre
cualquier real-nous en contexto test). Bind del socket Nous, accept
loop, despacha por `RequestKind`. EmbedFile usa
`nouser_core::embed::embed` (los pseudo-embeddings de Phase C).
Modelo: `mock-pseudo-32d`.
Cambios:
- `nouser-core`: dep nueva `nouser-nous`. Subcomando `attract` ahora
acepta `--remote` que abre un socket UnixStream blocking, envía un
`EmbedRequest` y lee la response. Imprime `embed: local|remote`
para que se vea cuál ruta corrió.
Validación end-to-end (un solo terminal, varios procesos):
$ ente-zero &
$ nouser-nous-mock &
$ NOUSER_MIN_FILES=5 nouser daemon crates/core &
$ brahman-status
Sessions (7):
[ente] nouser.nous_mock flows: embed-request, embed-result
[ente] brahman.nouser_engine
[data] src summary: 6 archivos en crates/core/brahman-handshake/src
[data] graph summary: 7 archivos en crates/core/ente-zero/src/graph
...
$ nouser attract --remote crates/core <archivo.rs>
embed: remote
🧲 0.9058 src ...
Mock log: "embed_file path=crates/modules/nouser/core/src/embed.rs"
Bug encontrado y corregido en el camino:
- `ContextBias` tenía `#[serde(skip_serializing_if = ...)]` en sus
campos. Postcard NO soporta skip-condicional (formato no
self-describing): el serializer omitía bytes que el deserializer
esperaba, rompiendo la wire de cualquier Card con
`priority_contexts` poblada.
- Fix: removidos los `skip_serializing_if` de `ContextBias`. JSON
pretty ahora emite `{"pin_to": null, "priority_offset": 0}` en lugar
de objeto vacío. Trade-off aceptado por compatibilidad de wire.
- Test nuevo en brahman-card: `wirecard_postcard_with_priority_contexts`
que ejercita el roundtrip completo postcard.
Tests acumulados: 75 (card 12 +1 nuevo, broker 15, handshake 9,
card-wit 4, admin 0, nouser-card 7, nouser-core 20, nouser-nous 2).
cargo check --workspace: 0 errores, 0 warnings.
Próximo natural: Phase D-2 — `real-nous` con un modelo ONNX/Llama de
text-embedding. La infraestructura ya está lista: declara la misma
Card con `priority_contexts.prod = { priority_offset: +1 }` y el
swap es transparente para el consumer.
### feat(nouser): Phase C — pseudo-embeddings + atracción por centroide
El "imán semántico" matemático del diseño Kairos, sin LLM. Cada
archivo se proyecta a un vector 32-d derivado de sus metadatos; cada