How Voice works¶
Voice is one ankka service, voice, written in Scala 3, and one
web-hosted interface, voice-web, which mounts the service at /api. The reasoning service,
reasoning from ankka-reasoning, runs beside them and holds the
evidence; Voice calls it as a service. The three run in one ankka project, and only voice-web is
exposed: a browser reaches voice through the mount and nothing else reaches it from outside.
Three endpoints, one per audience¶
| Endpoint | Prefix | Who | Access rule |
|---|---|---|---|
| public | /public |
anyone, no credential | open; no route creates anything, and no reply holds a sealed value or a participant id, which a build-time check enforces |
| citizen | /me |
a pseudonymous participant | a session token Voice itself mints and signs with VOICE_SESSION_KEY; the participant is the token's subject, never a value in a request |
| staff | /staff |
a reviewer, a representative or an operator | a person's OIDC token from the issuer named staff in the descriptor; the role is the token's, and each route states the role it needs |
The endpoint is the edge. It reads the clock once, reads the caller once, puts both into the command, and seals or opens personal values; nothing deeper has a clock, a caller or a key. Five audiences says what each may read.
The components, by stage¶
Every component is registered explicitly, and a test holds the registration equal to the list on Records.
- Hear. Event sourced entities
participant,conversationandconcern; the workflowconversation-turn, which asks theconcern-extractionagent for a reply and a proposal, off the request path. - Share. The event sourced entity
contribution; the workflowdisclosure-review, which runs the privacy detectors and theprivacy-flaggingagent, waits for a reviewer, and publishes; the key value entityissue-link, the only map from a public copy back to its author. - Understand. The event sourced entity
issue, which holds public copies, evidence links, options and the chronology; the key value entitiespolicy-optionandsource-card; the viewsissue-rowsandpublic-search. - Deliberate. The event sourced entity
expression; the viewissue-aggregates. - Act. Event sourced entities
investigation,action,decisionandoutcome; the workflowresearchwith theresearch-assistantagent; the entityaccess-log, one per day. - Return. Key value entities
interestandissue-followers; the event sourced entitynotification; the consumerdevelopmentsover issue events, which starts oneupdate-deliveryworkflow per development. - Erase. The workflow
erasure, keyed by participant; the key value entityparticipant-records, the index of every record a participant holds; the viewerasure-receipts; the timed actiondormancy-check.
What is kept where¶
| Store | Holds |
|---|---|
| the service's Postgres database | every entity's journal and state, every view, the workflows' state, and the secret store with one key per participant |
| the reasoning service's database | questions, hypotheses, sources, evidence and claims, as records that are never edited |
| the interface | nothing: a session token and a staff token in memory, for one tab |
Voice declares no topic and publishes nothing to a broker. The graph tier of the reasoning service, Kafka and Neo4j, is optional and adds answers Voice does not need; see Voice and the reasoning service.
Four rules the code holds to¶
- No personal value in an event, a state, a view row, a log or a reply, except inside a sealed field, or as the de-identified public copy a participant and a reviewer both approved. Sealed fields and erasure.
- No model call on a request path. A turn and a research run are workflows; the browser polls or holds a reconnecting stream. Nothing waits for a model.
- No clock and no caller in a handler. The endpoint reads both once and puts them in the command, as a provenance every event carries.
- Refusals by name. Every rule a command can break has a name, a status and the identifiers it is about, and a client matches on the name. Refusals.