Compare commits

Author SHA1 Message Date
Codex a59624a68f style: align global context panel and simplify selection copy 2026-09-13 10:43:42 +02:00
Codex 5af4408194 style: unify database block gutters and content alignment 2026-09-13 10:32:55 +02:00
Codex e088abd60a style: align catalog status with summary grid 2026-09-13 01:44:48 +02:00
Codex 803e9e9201 fix: remeasure composer after hidden Core becomes visible 2026-09-13 01:39:19 +02:00
Codex 8c81996896 style: place catalog status indicators after their labels 2026-09-13 01:23:46 +02:00
Codex eed398e569 style: restore prominent ThothII application wordmarks 2026-09-13 01:18:06 +02:00
Codex c8d276ddc6 style: unify workbench typography and prepare isolated visual review 2026-09-12 22:51:58 +02:00
Codex 2d1b714ebe fix: resolve admin issue review findings and record verification 2026-09-12 18:24:23 +02:00
Codex c7e5f295e6 fix: address administration layout and navigation issues #28 #29 #30 #31 2026-09-12 18:16:52 +02:00
Codex f52bf22e05 feat: establish unified administration and model context baseline 2026-09-12 18:03:15 +02:00
Codex 840344706f prototype: restore original Core within context shelf alternatives 2026-09-12 14:00:52 +02:00
Codex 41b9fed4d5 prototype: refine context shelf with remembered defaults and session tabs 2026-09-12 12:29:02 +02:00
Codex 36bf659ea9 prototype: explore global context with one operation at a time 2026-09-12 11:53:52 +02:00
Codex 4a67d60233 prototype: revisit five administration workflows after design review 2026-09-10 19:40:21 +02:00
Codex debb63d87b prototype unified administration page layouts 2026-09-10 16:48:25 +02:00
Codex 3943022a97 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	mkdocs.yml
2026-09-10 12:57:43 +02:00
Codex f5ec2d9313 docs: track security evidence and research notes 2026-09-10 12:53:13 +02:00
Codex 82e2c91f42 feat: implement memory and evidence administration with guided repairs
Publish documentation / publish (push) Successful in 1m27s
Add PostgreSQL-backed memory, editable evidence with source review and activation, and human-approved archive repairs across the harness, API, and UI. Include migrations, deployment support, regression coverage, and validation documentation.

Refresh permissions from validated session roles so existing administrator logins can access newly deployed archive management features.
2026-09-10 10:31:34 +02:00
User 8fe526dd6e fix(frontend): show session catalog in pi management 2026-09-08 14:58:18 +02:00
User e68e80a33d fix(frontend): prevent catalog header overlap 2026-09-08 14:35:36 +02:00
Codex 818563c408 fix(core): keep workspace runtime available 2026-09-08 13:37:10 +02:00
Codex 50c546e42d fix(frontend): accept catalog-owned workspace descriptors 2026-09-07 15:01:31 +02:00
Codex 651a5c7902 fix(frontend): isolate administration rail from portal CSS 2026-09-07 11:05:38 +02:00
User 28db30bd78 fix(ops): make server diagnostics release-safe 2026-09-07 01:15:28 +02:00
480 changed files with 20228 additions and 38726 deletions
+1
View File
@@ -17,6 +17,7 @@ frontend/vite.database-management-prototype.config.ts
!deploy/env/*.env.example
deploy/thothii.env
deploy/secrets/
deploy/psd/
harness/workspaces/*.yaml
!harness/workspaces/local.yaml
!harness/workspaces/tht.example.yaml
+104 -12
View File
@@ -91,21 +91,79 @@ correzione successiva crea una nuova sessione derivata, collegata a quella prece
dopo la finalizzazione. Non può modificare il ledger, gli artifact canonici o lo stato
terminale della sessione.
## Memory
**Memory Module** — Il modulo che possiede le conoscenze ed esperienze curate per
migliorare schema linking e generazione SQL di domande future. Le Memory appartengono
a un workspace e rimangono distinte dalle Evidence.
**Memory Card** — L'unità di contenuto gestibile del Memory Module, con identità,
ambito di applicazione e provenienza. Il formato è allineato per analogia alle
Evidence, senza implicare la stessa origine o lo stesso percorso di pubblicazione.
**Reusable Memory** — Una Memory Card che esprime un chiarimento di dominio, una
regola di costruzione SQL o un errore da evitare con motivo compreso e approvato.
La sua validità è circoscritta a un ambito esplicito e non deriva dalla sola
approvazione di una scelta occasionale in una domanda.
**Solved Question** — Una Memory Card che conserva una domanda risolta con la
relativa soluzione SQL e il contesto necessario a interpretarla. È un exemplar
consultativo: i parametri e le scelte del caso non diventano regole generali.
**Memory Graph** — L'insieme dei collegamenti espliciti fra card che contribuisce
al recupero di conoscenze pertinenti oltre alla somiglianza del contenuto. Il
ritrovamento di una card tramite un collegamento non ne implica l'approvazione.
**Memory Link** — Un collegamento curato fra card, con destinazione e significato
espliciti, che contribuisce alla consultazione di contenuti pertinenti. La sua
rimozione non comporta la cancellazione delle card collegate.
## Evidence
**Context specialist** — La persona competente sul dominio che redige e cura il
contenuto delle Evidence. Può essere distinta da chi amministra l'installazione;
il suo lavoro di redazione non richiede accesso al database applicativo.
**Evidence draft** — Il documento iniziale scritto dallo specialista di contesto,
che il sistema acquisisce e raffina in Evidence Unit. Può essere redatto e
consegnato indipendentemente dall'installazione che userà le Evidence risultanti.
**Evidence Module** — Il modulo autonomo che possiede la preparazione delle Evidence e
la loro consultazione durante il workflow. La preparazione avviene fuori dalle singole
sessioni; il workflow usa soltanto contenuti già pubblicati. A runtime contribuisce agli
stage semantici esistenti, senza diventare uno stage visibile e senza modificare ledger,
artifact o stato del workflow.
**Source Evidence** — Un documento originale del workspace, conservato senza modifiche
come riferimento umano e origine della successiva ristrutturazione.
**Source Evidence** — Il documento o la dichiarazione che sostiene il contenuto
corrente di una Evidence Unit. Un documento acquisito viene conservato come
riferimento umano; una dichiarazione manuale attribuisce il contenuto alla persona
che lo ha scritto e approvato.
**Manual Evidence declaration** — Una dichiarazione esplicita dell'amministratore
che sostiene una Evidence creata direttamente o una correzione del suo significato.
Non implica una verifica indipendente da parte di una fonte documentale esterna.
**Evidence origin** — Il documento da cui una Evidence Unit è stata inizialmente
derivata. Può restare collegato per provenienza e confronto con gli aggiornamenti
anche quando una dichiarazione manuale sostiene il testo corrente. La sola origine
non dimostra il supporto semantico di una successiva correzione.
**Local Evidence archive** — L'insieme delle Evidence curate custodite
dall'installazione, distinto dalle draft originali e dai contenuti derivati per
la ricerca. Comprende le correzioni manuali e i ritiri deliberati.
**Consolidated Evidence** — Una versione delle Evidence locali controllata come
insieme coerente e pronta per l'attivazione. I file ancora in modifica non ne
cambiano il contenuto.
**Active Evidence** — La versione consolidata disponibile alla consultazione del
core. Un tentativo di aggiornamento fallito conserva la versione attiva precedente.
**Evidence Unit** — La più piccola unità semantica coerente, revisionabile e ricercabile
derivata da una sola Source Evidence. Possiede un identificatore stabile indipendente
dal kind, assegnato una volta nella forma `evidence:<slug>`; fonti diverse non vengono
fuse automaticamente.
fondata su una Source Evidence corrente, anche manuale, e con eventuale origine
documentale distinta. Possiede un identificatore stabile indipendente dal kind,
assegnato una volta nella forma `evidence:<slug>`; fonti diverse non vengono fuse
automaticamente.
**Evidence kind** — La categoria semantica di una Evidence Unit, che ne determina i
campi specifici e ne orienta l'uso. Ogni unità ha un solo kind primario; i tipi iniziali
@@ -151,10 +209,9 @@ avanzare fino a un retry riuscito.
nella sessione: stage semantico, purpose, generazione interrogata e identificatori delle
Evidence restituite. Non duplica il contenuto delle Evidence.
**Curated Evidence** — Una o più Evidence Unit ristrutturate a partire da una Source
Evidence e conservate nel repository del workspace come proposte per la revisione
umana. Git conserva la versione precedente e rende visibile ogni modifica; una Curated
Evidence non è ancora contenuto autorevole del runtime.
**Curated Evidence** — Una o più Evidence Unit preparate da documenti o curate
manualmente. La presenza nell'archivio curato non implica da sola che il contenuto
sia già attivo per il workflow.
**Published Evidence** — Le Curated Evidence valide appartenenti alla revisione attiva
del workspace e alla generazione Evidence pubblicata. L'approvazione umana precede
@@ -176,9 +233,17 @@ una Evidence Unit. Il sistema ne verifica deterministicamente la presenza dopo l
normalizzazione meccanica; il curatore resta responsabile di verificarne la sufficienza
semantica.
**Evidence resolution** — L'operazione esplicita con cui un curatore ritira una
Evidence Unit oppure la ricollega a un Source Evidence esistente. Aggiorna documento e
manifest insieme, lascia un diff Git revisionabile e non pubblica né crea commit.
**Evidence resolution** — La decisione esplicita con cui un curatore risolve un
problema di una Evidence Unit, correggendola, ritirandola oppure ricollegandola a
una fonte adeguata.
**Source update conflict** — Un contrasto fra una fonte aggiornata e una correzione
manuale già approvata. La correzione resta in uso fino alla risoluzione esplicita
del confronto da parte dell'amministratore.
**Evidence source refresh** — La riacquisizione delle fonti esterne richiesta
dall'amministratore per rilevarne le modifiche. Fra due aggiornamenti il contenuto
già acquisito resta il riferimento per preparazione e consultazione.
**Review item** — Un blocco di revisione descritto da codice stabile, messaggio umano e
campo opzionale. Finché viene mantenuto nell'Evidence Unit, ne impedisce la
@@ -506,3 +571,30 @@ _Avoid_: Sensitive Data Suggestion Event
**Introspection Capability** — Una categoria di struttura fisica che una Database Binding
può osservare, come tabelle, colonne, relazioni, indici o enum. Una capability non disponibile
è distinta da una capability osservata che non ha restituito elementi.
## Amministrazione e integrazione
**Workspace Readiness** — La preparazione di uno specifico Workspace per l'uso nel
workflow, comprensiva della disponibilità degli artefatti derivati dai suoi metadati
Database e dalle sue Evidence. Il preprocessing appartiene a questa preparazione;
la configurazione e la sincronizzazione del catalogo restano responsabilità Database.
**Administration Surface** — Una superficie amministrativa autonoma per configurare o curare una
parte dell'installazione. Workspace, Evidence, Memory, Database e Pi sono superfici peer e non
dipendono dall'esistenza di una sessione attiva.
**Administration Page** — La rappresentazione a pagina intera di una Administration Surface, con
una gerarchia condivisa per identità, stato, azioni e contenuto. Un form amministrativo appartiene
alla pagina e non a una popup come contenitore principale.
_Avoid_: management popup, settings modal
**Administration Route** — L'identità navigabile di una Administration Surface nel browser. Deve
essere ripristinabile con refresh e cronologia e non contiene valori transitori o segreti dei form.
**Embedded Thoth Shell** — L'esperienza Thoth ospitata dentro il documento e il contesto visuale di
un portale host. Conserva la propria gerarchia funzionale, ma deve rispettare la geometria,
l'autenticazione e le regole responsive del portale host.
**Administrative Page Family** — L'insieme delle cinque Administration Page che condividono shell,
navigazione, tipografia e regole responsive, pur mantenendo contenuti e operazioni specifici:
Workspace, Evidence, Memory, Database e Pi.
+63 -42
View File
@@ -11,50 +11,50 @@ colors:
warm-graphite: "oklch(26.78% 0.0097 355.6)"
muted-graphite: "oklch(51.33% 0.0088 345.6)"
quiet-border: "oklch(90.93% 0.0035 354.7)"
success-mint: "oklch(75.77% 0.1581 165)"
success-mint: "oklch(46% 0.095 160)"
navigation-active: "oklch(92.5% 0.052 23.2)"
navigation-active-hover: "oklch(89.5% 0.071 23.2)"
navigation-active-foreground: "oklch(36.5% 0.11 23.2)"
navigation-active-border: "oklch(60% 0.135 23.2)"
warning-amber: "oklch(85.23% 0.1386 78.9)"
information-blue: "oklch(70.35% 0.1128 221.3)"
warning-amber: "oklch(48% 0.09 70)"
information-neutral: "oklch(51.33% 0.0088 345.6)"
typography:
display:
fontFamily: "Fraunces, Source Serif Pro, Georgia, Times New Roman, serif"
fontSize: "3rem"
fontFamily: "Manrope Variable, Manrope, system-ui, sans-serif"
fontSize: "1.5rem"
fontWeight: 600
lineHeight: 1.03
letterSpacing: "-0.025em"
headline:
fontFamily: "Fraunces, Source Serif Pro, Georgia, Times New Roman, serif"
fontSize: "1.875rem"
fontFamily: "Manrope Variable, Manrope, system-ui, sans-serif"
fontSize: "1.5rem"
fontWeight: 600
lineHeight: 1.15
letterSpacing: "-0.015em"
title:
fontFamily: "Fraunces, Source Serif Pro, Georgia, Times New Roman, serif"
fontSize: "1.2rem"
fontFamily: "Manrope Variable, Manrope, system-ui, sans-serif"
fontSize: "1.25rem"
fontWeight: 600
lineHeight: 1.25
letterSpacing: "-0.01em"
body:
fontFamily: "Manrope, -apple-system, BlinkMacSystemFont, Segoe UI, system-ui, Arial, sans-serif"
fontSize: "0.9375rem"
fontFamily: "Manrope Variable, Manrope, -apple-system, BlinkMacSystemFont, Segoe UI, system-ui, Arial, sans-serif"
fontSize: "1rem"
fontWeight: 400
lineHeight: 1.65
letterSpacing: "normal"
control:
fontFamily: "Manrope, -apple-system, BlinkMacSystemFont, Segoe UI, system-ui, Arial, sans-serif"
fontFamily: "Manrope Variable, Manrope, -apple-system, BlinkMacSystemFont, Segoe UI, system-ui, Arial, sans-serif"
fontSize: "0.875rem"
fontWeight: 600
lineHeight: 1.25
letterSpacing: "0.005em"
label:
fontFamily: "ui-monospace, SF Mono, Cascadia Code, Menlo, Consolas, monospace"
fontSize: "0.6875rem"
fontFamily: "Manrope Variable, Manrope, system-ui, sans-serif"
fontSize: "0.75rem"
fontWeight: 600
lineHeight: 1.25
letterSpacing: "0.06em"
letterSpacing: "normal"
rounded:
xs: "4px"
sm: "6px"
@@ -113,6 +113,16 @@ components:
# Design System: ThothII
## Visual review branch, September 2026
The revision on `codex/ui-visual-review` is approved for implementation and Docker visual review,
not yet for adoption on `main`. The previous look remains recoverable from the base commit and
the preserved Docker image. Historical prototypes must remain untouched.
This revision follows Impeccable's product register: one locally bundled Manrope family for the
whole UI, five fixed size roles, red as the sole brand accent and additional color only for meaningful
state. The primary scene remains an analyst reading data and SQL in a well-lit office.
## Overview
**Creative North Star: "The Clinical Workbench"**
@@ -134,7 +144,7 @@ disciplined and tactile, never playful, sluggish, or visually unstable.
**Key Characteristics:**
- Warm, restrained surfaces with one scarce red accent.
- Editorial headings paired with highly legible operational body text.
- One sans-serif family, with hierarchy expressed through size, weight and spacing.
- Dense information organized through hierarchy, rhythm, and progressive disclosure.
- Persisted artifacts and reviewer decisions presented as the visual source of truth.
- Fast state feedback with reduced-motion parity.
@@ -178,7 +188,8 @@ frontend uses OKLCH tokens directly.
foreground. It shares Instrument Red's hue but uses a lighter, lower-chroma fill, so location is
visible without carrying the full weight of a primary action.
- **Warning Amber** (`warning-amber`): waiting, attention, and in-progress states.
- **Information Blue** (`information-blue`): informational state when red would imply action.
- **Information**: neutral text and indicators for dates, protocols and ordinary status. The legacy
`--info` token resolves to muted foreground, not an additional blue accent.
The dark theme keeps the same semantic mapping with neutral near-black surfaces and a slightly
lighter red accent. Do not introduce a second visual identity for dark mode.
@@ -191,30 +202,33 @@ for their named states. Color is never the only state indicator.
## Typography
**Display Font:** Fraunces, with Source Serif Pro, Georgia, and Times New Roman fallbacks
**Body Font:** Manrope, with native system sans-serif fallbacks
**Label/Mono Font:** SF Mono or Cascadia Code, with Menlo and Consolas fallbacks
**UI Font:** locally bundled Manrope Variable, with Manrope and native sans-serif fallbacks.
**Technical Font:** SF Mono or Cascadia Code, with Menlo and Consolas fallbacks.
**Character:** Fraunces gives persisted artifacts and key headings editorial authority. Manrope
keeps dense controls and prose calm and readable. The mono register separates machine identity,
metadata, SQL, identifiers, and micro-labels from natural-language content.
Manrope covers headings, labels, controls, navigation and document reading. Monospace is reserved
for SQL, code, paths and machine identifiers, never for ordinary UI labels or status headings.
### Hierarchy
- **Display** (600, `3rem`, `1.03`): authentication and exceptional page-level statements only.
- **Headline** (600, `1.875rem`, `1.15`): major page or artifact titles.
- **Title** (600, `1.2rem`, `1.25`): panel and document section hierarchy.
- **Body** (400, `0.9375rem`, `1.65`): operational prose, with a target line length of 65 to 75
- **Headline** (600, `1.5rem`, `1.3`): page or artifact titles, `--text-page`.
- **Title** (600, `1.25rem`, `1.4`): section hierarchy, `--text-section`.
- **Body** (400, `1rem`, `1.6`): operational prose, `--text-body`, with a target line length of 65 to 75
characters where the surface controls width.
- **Control** (600, `0.875rem`, `1.25`): buttons, inputs, tabs, and compact actions.
- **Label** (600, `0.6875rem`, `0.06em` tracking): uppercase micro-labels, state metadata, and panel
headers. Labels use the mono family.
- **Control** (400–600, `0.875rem`, `1.5`): buttons, inputs, tables, tabs and compact subheadings,
`--text-control`.
- **Metadata** (400–600, `0.75rem`, `1.5`): secondary status, counts and timestamps, `--text-meta`.
Labels use sentence case and normal tracking. Ordinary operational text never falls below 12px.
Typography uses fixed sizes. Responsive changes happen at structural breakpoints, not through fluid
type scaling. Numeric data and identifiers use tabular numerals where comparison matters.
**The Three Registers Rule.** Serif means authority, sans means interaction and reading, mono means
machine identity. Do not exchange these roles for novelty.
**Application wordmark:** ThothII is a brand mark, not a page title: use Manrope semibold at
48px (`3rem`) in the Core welcome area and 32px (`2rem`) in the session sidebar, with the
`II` suffix in brand red. Preserve these sizes across responsive layouts.
**The One Family Rule.** The UI and document readers use sans-serif throughout. The legacy
`--font-heading` alias resolves to `--font-sans`. Preserve technical monospace without turning it
into a second decorative hierarchy. Do not shrink text to solve layout constraints.
**The Read Once Rule.** A heading, label, and body must be distinguishable on first glance through
size and weight. Do not repeat headings in explanatory copy.
@@ -279,22 +293,26 @@ default, hover, focus, active, disabled, loading, and error behavior where those
- **Focus:** three-pixel Instrument Red ring with a clear border shift.
- **Error / Disabled:** errors combine destructive color with explanatory text; disabled controls
retain readable contrast and use 50 percent opacity.
- **Metadata catalog model:** Database Management keeps one compact, installation-level
metadata-generation LLM selector in the application header. The selection persists across
database, table, column, and relationship views; when no usable profile is configured, the
disabled control explains: “No metadata-generation LLM model is configured for this installation.”
- **Global context:** the collapsible top shelf is the sole workspace/model selector for Core and
Admin. Preserve independent remembered choices, installation defaults, operation locks and unsaved
edit guards. Never introduce a separate metadata-generation default or selector.
### Navigation
- **Style:** compact session rows use `8px` corners and restrained vertical padding.
- **Default / Hover / Active:** porcelain at rest, Sunken Surface on hover, and a muted Navigation
Active red with a defined border when current. Exactly one top-level navigation control is current.
- **Administrative controls:** the admin-only Administration accordion groups Database management,
a structural divider, Workspace management, and Pi management in that order. Its trigger exposes
- **Administrative controls:** the admin-only Administration accordion groups Database,
Memory, Evidence, a structural divider, Workspace, and Pi configuration in that order. Its trigger exposes
expanded state and starts collapsed by default, while non-admin users do not receive the accordion
or its navigation actions.
- **Responsive:** collapse navigation structurally at the application breakpoint. Do not shrink
labels into illegibility.
labels into illegibility. Below 768px, Memory and Evidence management use the full content
width; a Navigation button opens the shared accessible dialog. Selecting another archive
page or pressing Escape closes it. Desktop retains the right session sidebar and its My sessions /
All sessions tabs. Core retains question/answer, eight phases, reviewer gates and the left log.
The portal owns the red header and left sidebar; ThothII must not duplicate them. Size to the
actual application container. Narrow session document panels may use the available width.
### Tabs
@@ -320,8 +338,9 @@ default, hover, focus, active, disabled, loading, and error behavior where those
### Curated Evidence Documents
Curated evidence follows a fixed reading order: title, compact type and purpose summary, scope,
typed content, supporting excerpts, review items, then collapsed technical provenance. Machine
metadata stays in invisible comments so GitHub Preview shows only the reviewable document.
typed content, supporting excerpts, review items, then technical provenance. Curated v4 files
use short, visible YAML frontmatter for identity and classification. The Markdown title and
body are authoritative; hidden payload comments are a legacy format converted on consolidation.
`applies_to` is rendered as “Ambito di applicazione” with separate bullet lists for concepts,
tables, and columns. Enum values also use lists. Tables are forbidden for metadata, scope, or any
@@ -329,7 +348,9 @@ one-dimensional collection; reserve tables for genuinely two-dimensional dataset
identifiers use inline code. SQL uses fenced code. Supporting excerpts use blockquotes.
**The Review Surface Rule.** The visible Markdown must be readable without understanding the
machine contract. Technical metadata belongs in progressive disclosure, not above the title.
machine contract. In Administration, explain current and original provenance separately and
keep file-editing templates and Git instructions in progressive disclosure. Show actual host
paths with copy controls, never browser file links to container-only locations.
## Do's and Don'ts
+229 -2
View File
@@ -1,6 +1,6 @@
# ThothII — Project State
Last updated: 2026-09-06.
Last updated: 2026-09-12.
This file is the short operational snapshot. Stable commands and the architecture mental model
live in `AGENTS.md`; current design and runtime contracts live under `docs/architecture/`,
@@ -14,6 +14,53 @@ requirements as mandatory; do not replace the running server stack in place.
## Current product shape
### Isolated visual review, awaiting owner acceptance
The UI revision is isolated in `/Users/mp/projects/ThothII-visual-review`, branch
`codex/ui-visual-review`, based on `2d1b714e`. The original checkout and its prototypes
remain intact. No merge to `main` has been performed.
Local Docker at `http://127.0.0.1:8080/` now runs the frontend image
`thothii-frontend:visual-review-20260912`. Only frontend was recreated; Core and all
data services/volumes/configurations are unchanged. The prior look is preserved as
`thothii-frontend:before-visual-review-20260912`. All five running services are healthy.
The revision uses bundled Manrope throughout, shared type roles, restrained semantic
colors, clearer Admin copy and a readable compact session-document panel. Core and
session behavior remain covered by the existing regression suite. Verification:
679 frontend tests, 7 Playwright visual/interaction scenarios and a production build.
See `docs/reports/2026-09-12-ui-visual-review-delivery.md` for scope, limits and the
exact local rollback command. Visual approval is required before adoption on `main`.
Gitea #28–#31 follow-up is implemented in this checkout: Workspace's four tabs,
Database list-first entry without the preparation footer, full-height Pi instructions
with installation-host OS selection, and short Admin navigation labels. Core and
session behavior are retained. At the owner's request, these changes were rebuilt into
local Docker on 2026-09-12 at 18:31 UTC. Core/frontend are healthy and the UI at
`http://127.0.0.1:8080` serves the updated bundle. The host projection now supplies
`THT_HOST_PLATFORM=darwin`, so Pi selects macOS rather than the container's Linux OS.
Persistent dependency containers and volumes were unchanged. Rollback images are tagged
`thothii-core:before-admin-28-31-20260912` and
`thothii-frontend:before-admin-28-31-20260912`. See
`docs/reports/2026-09-12-admin-issues-28-31.md` for verification and deployment details.
The latest context-shelf A and five Administration pages are implemented locally.
At the owner's request, local Docker project `thothii-18998cca7b0a` was rebuilt and
its core/frontend recreated on 2026-09-12. The real UI at `http://127.0.0.1:8080`
serves shelf A; both services and their existing dependencies are healthy.
Runtime model projections were regenerated as schema v2 with the single
`zai/glm-5.3` interaction default. Persistent services/volumes were not recreated.
The local launcher `/private/tmp/thothii-memory-preview.sh` now adds
`/private/tmp/thothii-context-a.compose.yaml` last, building from this checkout
instead of the earlier Memory worktree. Previous images are retained under
`thothii-core:before-context-a-20260912` and
`thothii-frontend:before-context-a-20260912`. Remote server deployment remains pending.
Core/session behavior is retained; one independently remembered workspace/model
pair controls both Core and Admin. Unsaved Admin changes block navigation and
context changes; operation activity locks the selectors. See
`docs/reports/2026-09-12-context-shelf-a-implementation.md` for verification and the
remaining server/Omics integration gate. Prototype alternatives remain untouched.
ThothII is a human-in-the-loop datamart builder with three independently built layers:
```text
@@ -26,6 +73,165 @@ metadata catalog for administrative database configuration. The frontend renders
and keeps the live transcript in memory. See
`docs/architecture/components.md` for the detailed component and data-flow map.
## Memory M1–M3, Evidence E1–E3 and joint workflow repair X1 implemented
Browser authorization now derives permissions from validated session roles using the current
catalog. This fixes Memory/Evidence navigation remaining disabled for administrators whose
remembered login predates those permissions; refreshing the page loads the updated permissions.
The owner requested two independent administration projects: Memory management and Evidence
management. Their navigation entries must sit immediately after Database management, as peers;
neither page belongs to Database management. Both require complete browsing, filtering, and CRUD
without an active core session. Shared requirements and the two project briefs are linked from
`docs/plans/2026-09-08-memory-evidence-administration.md`. Memory administration is implemented;
Evidence administration is implemented through external file editing and explicit consolidation.
Evidence editing requires an explicit evolution of the current authoring/publication contract.
The M1 specification is at `docs/plans/2026-09-08-memory-m1-spec.md` and published as
[M1 — Archivio autorevole e amministrazione delle Memory Card](https://git.tylconsulting.it/mptyl/ThothII/issues/27)
with the `ready-for-agent` and `enhancement` labels.
It covers the authoritative store, administrative CRUD, projection recovery, and current
Memory/exemplar producer integration. Its accepted test boundaries are the public harness
service, Fastify APIs, and the AppShell page, joined by a focused real-stack browser path.
The owner confirmed those boundaries and authorized publication on 2026-09-08.
M1 is implemented locally: PostgreSQL authority in `thoth_memory`, versioned installation
migrations, admin CRUD for all four families, structured dependencies and links, explicit
projection recovery, verified recall and current workflow producers. The page requires no
active session or DWH binding. The existing `catalog-migrate` preparation service now runs
Memory migrations as well. Existing installations need that preparation before using M1;
the initial implementation did not deploy or migrate the owner's stacks.
The integrated browser check passed with real authentication, Fastify, ThtRunner, harness,
PostgreSQL and Qdrant; embeddings were deterministic and unrelated Pi/session activity used
test fixtures. See `docs/plans/2026-09-08-memory-m1-validation.md` for results and commands.
M2 is implemented locally: Memory dense/BM25 fusion, physical and business scope filters,
bounded outgoing-link expansion and joint ranking, all resolved against current PostgreSQL
authority. Migration `002_hybrid_projection.sql` makes old dense projections pending until
explicit retry/rebuild; Reference remains separate. The real retrieval check uses the configured
`qwen3-embedding:0.6b` model, a separate Ollama process with a read-only model-volume mount,
and isolated PostgreSQL/Qdrant resources. See `docs/plans/2026-09-09-memory-m2-validation.md`.
M3 is implemented: editable F8 summary grounded in effective approved decisions, explicit
updates with concurrent-edit protection, selected-card/link transactions and durable review
receipts. Finalization no longer saves exemplars implicitly. SQL rules and explained errors
are consulted in the existing F4/F6/F7 gates. Successful Catalog physical synchronization
performs dependency cleanup, preserving the original removals for recovery across restarts.
Migration `003_review_receipts.sql` is required. Validation includes a real GLM 5.3 generation
case against synthetic PostgreSQL data; see `docs/plans/2026-09-09-memory-m3-validation.md`.
Evidence administration and the joint X1 conflict-repair increment are now implemented.
The same local preview was subsequently rebuilt with M3 and migration 003 applied.
Core and frontend now include the final Memory review and physical dependency cleanup.
On 2026-09-09, at the owner's request, the local PSD Docker installation
`thothii-18998cca7b0a` was updated from this worktree. Core/frontend images were rebuilt,
Catalog and Memory migrations completed, and all five services became healthy. The UI is at
`http://127.0.0.1:8080`, using the existing local authentication and persistent volumes.
The `psd-clinical` authoritative Memory archive is initially empty (no legacy import).
The existing installation configuration remains in `/Users/mp/projects/ThothII/deploy/psd/`;
`/private/tmp/thothii-memory-preview.sh` invokes its Compose files with a final build-context
and migration-command override from this worktree. A future build from the main checkout
will use that checkout's code, so retain the worktree override until the changes are integrated.
Memory revision history and compatibility with existing development sessions are not requirements.
The agreed Memory scope includes reusable domain clarifications, SQL construction rules, solved
questions, and explained, approved mistakes to avoid. This extends the current runtime's
`concept_clarified`-only reusable Memory contract. The owner also approved an editable final summary
for proposed additions/updates and Memory consumption in the relevant existing review gates.
Further agreed behavior includes persistent corrections for Memory/Evidence conflicts through
explicit choices, deletion of Memory with invalid dependencies after successful physical schema
synchronization, and bounded functional/regression tests instead of a general quality benchmark.
In-house Memory evolution is approved, including hybrid Qdrant retrieval and explicit card links
traversed in core, without a dedicated graph database. Both capabilities belong to the current
scope. PostgreSQL is the agreed authority for Memory Cards, links, and schema dependencies;
Qdrant is a rebuildable index. Links are reviewed with cards and editable in Administration;
deleting a card removes its incident links while preserving the other cards. The accepted,
implemented architecture is recorded in
`docs/adr/0018-use-postgres-for-memory-and-qdrant-for-retrieval.md`.
For Evidence administration, Q12 rejects a separate editorial publication workflow.
The final R0 direction uses external editors and a manual consolidation command that
validates structure, reports required corrections, updates derived metadata, and
activates the local Evidence index. The operator then checks the diff and runs Git
commit/push manually. No watcher, automated Git, or web editor is required.
The owner's later simplification
instruction removes the requirement to support administrative edits while core work
is in progress. Deliberate corrections made by the workflow itself remain supported.
The context specialist writes Evidence drafts independently of the installation and
without PostgreSQL access; the system refines them and stores them locally for use
and maintenance. The revised Q11 recommendation separates external drafts from a
durable local canonical file archive, removing automatic commit/push from CRUD.
The owner has now accepted local files and requires discoverable, editable Markdown
for nontechnical domain specialists, with no JSONL management surface. Editors on
Mac/PC or vim/nano on the server are the chosen R0 editing surface. Evidence
management keeps browsing/filtering/detail and clearly identifies the persistent
working tree, each Markdown file's actual host path, and the manual commands.
E1 implements editable Curated Evidence v4, deterministic legacy conversion, persistent
local files and a consolidation API with immutable candidates, manual provenance,
deletion records and recoverable activation. E2 connects its installed command, runtime source
selection and administration page. The operator completes Git steps manually.
All 35 PSD units were converted on an isolated copy with identical IDs and typed content.
Tests exercise visible edits through normalization, indexing and recall with real Qdrant;
see `docs/plans/2026-09-09-evidence-e1-validation.md` and
`docs/contracts/curated-evidence-v4.md`. E2 is now running on the local Docker preview:
all 35 PSD units were converted and indexed through the installed command. The editable
host archive is `/Users/mp/projects/ThothII/deploy/psd/evidence-registry/repo/psd-clinical/evidence`.
The original registry-volume checkout and original author repository were retained.
The installation descriptor now includes `workspace-bindings.yaml` and `evidence-host.yaml`
so native maintenance uses the same volumes and checkout as the preview. Descriptor backup:
`/private/tmp/thothii-installation-before-e2.yaml`; previous native binary: `/private/tmp/tht-before-e2`.
The launcher remains `bash /private/tmp/thothii-memory-preview.sh`; its worktree build override
is still required until integration. Runtime lease filenames now include rendered bytes so
upgrading the Evidence renderer does not collide with old immutable configs; Catalog
input fingerprints and readiness are unchanged. See `docs/plans/2026-09-09-evidence-e2-validation.md`.
E3 adds explicit source acquisition/refinement and durable comparisons in Evidence management.
Local drafts go in `evidence/incoming/`; original local documents and configured HTTP/S3
sources are reacquired only by **Import or refresh sources** or installed
`tht workspace evidence refresh --workspace <id>`. Keep/replace decisions, including the
native `workspace evidence decide` command, activate through the existing archive/index
path and have durable retry state. Acquired source versions and remote provenance stay
local; old documentary lineage can coexist with current manual declarations.
The installed preview's 35 PSD sources were verified unchanged with no new proposals.
The previous native CLI is backed up at `/private/tmp/tht-before-e3`.
See `docs/plans/2026-09-09-evidence-e3-validation.md` for tests and validation limits.
X1 adds `reviewer_archive_repair`: closed alternatives with complete before/after content,
explicit rejection/reformulation, admin-only application, durable session receipts and
retry of saved but inactive corrections. The coordinator uses the canonical Memory and
Evidence services; phase approval remains separate. Migration `004_archive_repairs.sql`
is required. See `docs/contracts/archive-repair.md` for authorization and recovery limits.
X1 validation is recorded in `docs/plans/2026-09-09-archive-repair-x1-validation.md`:
both archives were corrected and retrieved through the actual CLI with real PostgreSQL
and Qdrant; desktop/mobile widget behavior and permission failures were verified separately.
The local preview images include X1 and migration 004 is applied. Follow-up technical
acceptance passed with configured GLM 5.3 generating closed conflict alternatives and
the chosen Memory correction persisted and retrieved. An authenticated browser test
also verified both administration pages, navigation order, Evidence filtering, workspace
404s, desktop/mobile layout, and Evidence survival after Memory deletion. All approved
technical increments/checks are complete. A real PSD domain-conflict session remains
the reviewer's semantic acceptance check; automated cases did not modify PSD knowledge.
The joint browser inspection also fixed mobile archive navigation: below 768px,
Memory and Evidence keep the full content width and open navigation in the shared
accessible dialog. Desktop retains its sidebar; the local frontend image includes this fix.
The owner accepted the remaining simplifications and requested explicit clarification
of the core format change and the simple terminal-based Git check. E1 must adapt
the Evidence parser, renderer, authoring, validation, and normalization; convert
existing files and reindex; and verify that visible edits reach core consumption.
Preserve the internal typed model where possible. This precedes the administrative
page and is not merely a presentation change. Human inspection uses normal Git
status and optional line-level diff commands; no custom diff viewer or mandatory
double review is required.
The suggestion of making PostgreSQL the Evidence authority was withdrawn after this
clarification; it was never implemented or accepted as a replacement for Q11.
Q13 keeps manual corrections active when updated sources contradict them, until an
administrator resolves the comparison; deleted Evidence must not be regenerated
automatically. Q14 allows direct manual creation and records a manual declaration
as the current source, preserving any original document as distinct provenance.
Q15 refreshes external sources only on explicit administrator request; normal saves
and lookups do not reacquire them. These decisions, now implemented through E1–E3, are recorded in
`docs/adr/0019-author-evidence-in-app-with-automatic-activation.md`.
The decision-by-decision review is recorded in
`docs/plans/2026-09-08-memory-evidence-simplification-review.md`; it distinguishes the
new constraints from the revised technical recommendations. It also specifies a
sequential save with minimal durable retry state and direct Memory cleanup after
successful schema synchronization, without new queues or event infrastructure.
The delivery order remains Memory, Evidence, and persistent conflict repair between
both modules. Local curated Evidence must survive preprocessing Clear and be backed
up as primary data; the Qdrant projection remains rebuildable.
No runtime gate change or administrative page is implemented yet.
## Evidence restructuring — accepted
The evidence restructuring and PSD migration completed real acceptance on 2026-08-25.
@@ -93,7 +299,28 @@ regenerates the backend catalog, Pi `models.json`/`settings.json`, and Compose o
installation-local `generated/` directory. Those projections are replaceable runtime adapters:
they are not edited, backed up, or treated as configuration.
Session and metadata defaults use canonical `provider/model` IDs. Provider authentication declares
Core and Administration now share one canonical `modelCatalog.defaults.interaction` per installation,
independent of workspace. Runtime catalog schema v2 contains only `defaultInteraction`; apply the host,
backend, and regenerated projections together. Equal legacy defaults normalize on read; conflicting
ones require an explicit operator choice. With Admin AI configured, selectable models are the
intersection of the Pi and LiteLLM adapters; Core-only installations remain supported without Admin AI.
The existing Core and Database controls share the operational selection. Explicit model choices are
remembered per authenticated user/application mount in this browser, not in installation settings.
Resume uses that selection/default while preserving the historical workspace/revision and manifest.
Every model must be manually exercised in both Core and Admin as documented in
`docs/general/pi-configuration.md`; validation is not a live model certification.
These changes and shelf A are now deployed to local Docker; remote deployment remains pending.
PSD DeepSeek Pro/Flash now share canonical `deepseek/...` identities across native Pi and LiteLLM,
using the existing `DEEPSEEK_API_KEY` bundle entry. Its value was confirmed identical to the working
Pi key without exposing it; no secret files were changed. The duplicate `deepseek-metadata` descriptor
is removed locally and the tracked example uses the shared provider. `secret_env` overrides legacy
Pi auth only inside temporary runtime snapshots and fails closed if the bundle key is missing.
The original auth store/history and `zai/glm-5.3` default are preserved. Local Docker projections
and core/frontend were updated together on 2026-09-12. Regenerate projections with
the matching release for the separate server deployment.
Provider authentication declares
one explicit mode (`secret_env`, `pi_auth`, or `none`); `secret_env` names a protected bundle key.
The backend settings store now owns only the selected workspace and thinking level. Existing v1
installations use the explicit catalog migration command; schema-v3 workspace descriptors are
+18 -1
View File
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
import { tmpdir } from "node:os";
import type { AppConfig } from "./config.js";
import { ThtRunner } from "./tht/tht-runner.js";
import { createMemoryCleanup } from "./catalog/memory-cleanup.js";
import { PiProcessManager } from "./pi/pi-process-manager.js";
import { SseHub } from "./sse/sse-hub.js";
import { authenticateSession, captureAuthConfigSnapshot, configuredOrigin } from "./auth/auth.js";
@@ -80,6 +81,8 @@ import { createProductionWorkspacePreprocessingService } from "./workspace-maint
import type { WorkspacePreprocessingService } from "./workspaces/preprocessing-service.js";
import { PreprocessingStateStore } from "./workspaces/preprocessing-state.js";
import { workspacePreprocessingRoutes } from "./routes/workspace-preprocessing.js";
import { memoryRoutes } from "./routes/memory.js";
import { evidenceRoutes } from "./routes/evidence.js";
export interface BuildAppDeps {
thtRunner?: ThtRunner;
@@ -93,7 +96,7 @@ export interface BuildAppDeps {
workspaceDiagnoser?: WorkspaceDiagnoser;
workspaceDatabaseTester?: WorkspaceDatabaseTester;
workspaceSecretStore?: WorkspaceSecretStore;
workspacePreprocessingService?: Pick<WorkspacePreprocessingService, "run" | "clear">;
workspacePreprocessingService?: Pick<WorkspacePreprocessingService, "run" | "clear"> & Partial<Pick<WorkspacePreprocessingService, "consolidateEvidence" | "evidenceSources">>;
catalogRepository?: CatalogRepository;
catalogService?: CatalogService;
catalogPostgresAccess?: CatalogPostgresAccess;
@@ -265,6 +268,11 @@ export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstanc
catalogSchemaIntrospector,
catalogOperationCoordinator,
config.catalogSyncTimeoutMs,
createMemoryCleanup(tht as ThtRunner, {
internalQdrantUrl: config.internalQdrantUrl, internalEmbeddingUrl: config.internalEmbeddingUrl,
internalEmbeddingId: config.internalEmbeddingId, internalEmbeddingModel: config.internalEmbeddingModel,
internalEmbeddingDimensions: config.internalEmbeddingDimensions,
}),
);
app.addHook("onReady", async () => { await catalogSyncWorker.initialize(); });
app.addHook("onReady", async () => { await descriptionGenerationWorker.initialize(); });
@@ -493,7 +501,16 @@ export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstanc
return maintenanceBarrier.status();
});
sqlRoutes(app, { tht: tht as ThtRunner, getSettings, workspaceRegistry });
memoryRoutes(app, { runner: tht as ThtRunner, registry: workspaceRegistry, runtime: {
internalQdrantUrl: config.internalQdrantUrl,
internalEmbeddingUrl: config.internalEmbeddingUrl,
internalEmbeddingModel: config.internalEmbeddingModel,
internalEmbeddingDimensions: config.internalEmbeddingDimensions,
} });
metaRoutes(app, { harnessDir: config.harnessDir, modelCatalog: runtimeModelCatalog });
evidenceRoutes(app, { runner: tht as ThtRunner, registry: workspaceRegistry,
registryRoot: config.workspaceRegistry.root, hostRegistryRoot: config.evidenceHostRegistryRoot,
service: workspacePreprocessingService });
workspaceRoutes(app, {
registry: workspaceRegistry,
config: config.workspaceRegistry,
+3 -1
View File
@@ -119,7 +119,9 @@ export function authenticateSession(deps: AuthDependencies): preHandlerHookHandl
subject: session.subject,
...(session.displayName === undefined ? {} : { displayName: session.displayName }),
roles: session.roles,
permissions: session.permissions,
// Sessions can outlive a deployment that changes the role permission catalog.
// resolve() has already checked validity, including current local user roles.
permissions: rolesToPermissions(session.roles),
isAdmin: session.roles.includes("admin"),
};
if (STATE_CHANGING_METHODS.has(request.method)) {
+1 -1
View File
@@ -38,7 +38,7 @@ const MAX_MAPPED_GROUPS = 128;
const ROLES = ["user", "admin"] as const;
export const PERMISSION_CATALOG: readonly Permission[] = [
"session.use", "session.read_all", "session.manage_all", "settings.manage",
"workspace.manage", "workspace.secrets.manage", "database.manage", "pi.manage", "auth.diagnostics.read",
"workspace.manage", "workspace.secrets.manage", "database.manage", "memory.manage", "evidence.manage", "pi.manage", "auth.diagnostics.read",
];
const invalid = (): Error => new Error("authentication configuration is invalid");
+1 -1
View File
@@ -33,7 +33,7 @@ const EMPTY_HKDF_SALT = Buffer.alloc(0);
const ROLES = ["user", "admin"] as const;
const PERMISSIONS = [
"session.use", "session.read_all", "session.manage_all", "settings.manage",
"workspace.manage", "workspace.secrets.manage", "database.manage", "pi.manage", "auth.diagnostics.read",
"workspace.manage", "workspace.secrets.manage", "database.manage", "memory.manage", "evidence.manage", "pi.manage", "auth.diagnostics.read",
] as const satisfies readonly Permission[];
const invalid = (): Error => new Error("auth_session_store_invalid");
+1 -1
View File
@@ -5,7 +5,7 @@ export type Role = "user" | "admin";
export type Permission =
| "session.use" | "session.read_all" | "session.manage_all"
| "settings.manage" | "workspace.manage" | "workspace.secrets.manage"
| "database.manage" | "pi.manage" | "auth.diagnostics.read";
| "database.manage" | "memory.manage" | "evidence.manage" | "pi.manage" | "auth.diagnostics.read";
export interface AuthenticationSessionConfig {
regularTtlSeconds: number;
+23
View File
@@ -0,0 +1,23 @@
import type { ThtRunner } from "../tht/tht-runner.js";
import type { SemanticRuntimeConfig } from "../workspaces/runtime-renderer.js";
import type { CatalogSyncRun, WorkspaceDatabase } from "./types.js";
/** Internal continuation of an applied physical sync, using the harness Memory boundary. */
export function createMemoryCleanup(runner: Pick<ThtRunner, "withPrincipal">, runtime: SemanticRuntimeConfig) {
return async (database: WorkspaceDatabase, run: CatalogSyncRun): Promise<number> => {
if (run.phase !== "memory_cleanup" || !run.plannedDiff) throw new Error("Physical cleanup is not committed");
const result = await runner.withPrincipal({ issuer: "installation", subject: "catalog-sync",
roles: ["admin"], permissions: ["memory.manage"], isAdmin: true,
}).runWithRuntimeSnapshot(["memory", "admin", "--workspace", database.workspaceId], JSON.stringify({
action: "cleanup", runtime, request: { sync_id: run.id, database: database.databaseName,
schema_name: database.schema, removed_tables: run.plannedDiff.deletedTables,
removed_columns: run.plannedDiff.deletedColumns.map(column => ({ table: column.tableName, column: column.columnName })),
},
}));
const payload = JSON.parse(result.stdout);
if (result.code !== 0 || payload.indexed !== true || !Number.isInteger(payload.deleted) || payload.deleted < 0) {
throw new Error("Memory cleanup is incomplete");
}
return payload.deleted;
};
}
+3 -1
View File
@@ -926,6 +926,7 @@ export class MemoryCatalogRepository implements CatalogRepository {
scope: CatalogSyncScope,
tableIds: readonly string[],
snapshot: ObservedSchemaSnapshot,
syncRunId?: string,
): Promise<CatalogSyncCounts | undefined> {
const database = this.records.get(databaseId);
if (!database || database.version !== expectedDatabaseVersion) return undefined;
@@ -1141,6 +1142,7 @@ export class MemoryCatalogRepository implements CatalogRepository {
if (scope === "all") {
this.records.set(databaseId, { ...database, schemaSyncedVersion: expectedDatabaseVersion, schemaSyncedAt: now });
}
if (syncRunId) await this.updateSyncRun(syncRunId, { phase: "memory_cleanup" });
return {
tables: (await this.listTables(databaseId)).length,
columns: [...this.columns.values()].filter((column) => this.tables.get(column.tableId)?.databaseId === databaseId).length,
@@ -1254,7 +1256,7 @@ export class MemoryCatalogRepository implements CatalogRepository {
for (const run of this.syncRuns.values()) {
if (["queued", "running", "awaiting_confirmation", "applying"].includes(run.state)) {
await this.updateSyncRun(run.id, {
state: "interrupted", phase: "completed", finishedAt: new Date().toISOString(),
state: "interrupted", phase: run.phase === "memory_cleanup" ? "memory_cleanup" : "completed", finishedAt: new Date().toISOString(),
errorCode: "SYNC_INTERRUPTED", errorMessage: "Synchronization was interrupted by a service restart",
});
}
@@ -102,7 +102,7 @@ export function loadMetadataGenerationModels(options: {
...(apiKeyEnv ? { apiKeyEnv, apiKey } : {}),
}));
}
const result = new RestartLoadedMetadataGenerationModels(models, catalog.defaultMetadataGeneration);
const result = new RestartLoadedMetadataGenerationModels(models, models.size ? catalog.defaultInteraction : null);
const safe = result.catalog();
return {
catalog: () => ({
+6 -1
View File
@@ -1604,6 +1604,7 @@ export class KyselyCatalogRepository implements CatalogRepository {
scope: CatalogSyncScope,
tableIds: readonly string[],
snapshot: ObservedSchemaSnapshot,
syncRunId?: string,
): Promise<CatalogSyncCounts | undefined> {
return await this.db.transaction().execute(async (trx) => {
const database = await trx.selectFrom("workspaceDatabases").select("version")
@@ -1779,6 +1780,10 @@ export class KyselyCatalogRepository implements CatalogRepository {
schemaSyncedAt: now,
}).where("id", "=", databaseId).execute();
}
if (syncRunId) {
await trx.updateTable("catalogSyncRuns").set({ phase: "memory_cleanup" })
.where("id", "=", syncRunId).where("databaseId", "=", databaseId).execute();
}
return {
tables: snapshot.tables.length,
columns: scope === "tables" ? undefined : snapshot.columns.length,
@@ -1882,7 +1887,7 @@ export class KyselyCatalogRepository implements CatalogRepository {
async interruptActiveSyncRuns(): Promise<void> {
await this.db.updateTable("catalogSyncRuns").set({
state: "interrupted", phase: "completed", errorCode: "worker_restarted",
state: "interrupted", phase: sql`case when phase='memory_cleanup' then phase else 'completed' end`, errorCode: "worker_restarted",
errorMessage: "Synchronization was interrupted by a backend restart.",
finishedAt: sql`now()`, updatedAt: sql`now()`,
leaseOwner: null, leaseExpiresAt: null,
+46 -19
View File
@@ -58,6 +58,8 @@ export class CatalogSyncWorker {
private readonly introspector: CatalogSchemaIntrospector,
private readonly operations: CatalogOperationCoordinator,
private readonly timeoutMs: number,
private readonly cleanupMemory: (database: WorkspaceDatabase, run: CatalogSyncRun) => Promise<number>
= async () => 0,
) {}
async initialize(): Promise<void> {
@@ -67,6 +69,9 @@ export class CatalogSyncWorker {
}
async start(database: WorkspaceDatabase, scope: CatalogSyncScope, tableIds: readonly string[]): Promise<CatalogSyncRun> {
if ((await this.repository.listSyncRuns(database.id, 100)).some(run => run.phase === "memory_cleanup")) {
throw new CatalogConflictError("Retry the pending Memory cleanup before starting another synchronization");
}
const uniqueTableIds = [...new Set(tableIds)];
if (scope === "columns") {
const tables = await Promise.all(uniqueTableIds.map((tableId) => this.repository.getTable(database.id, tableId)));
@@ -109,7 +114,7 @@ export class CatalogSyncWorker {
async cancel(runId: string): Promise<CatalogSyncRun | undefined> {
const run = await this.repository.getSyncRun(runId);
if (!run) return undefined;
if (run.state === "applying" || TERMINAL_STATES.has(run.state)) return run;
if (run.phase === "memory_cleanup" || run.state === "applying" || TERMINAL_STATES.has(run.state)) return run;
await this.repository.requestSyncRunCancellation(runId);
this.controllers.get(runId)?.abort();
if (run.state === "queued" || run.state === "awaiting_confirmation") {
@@ -138,6 +143,16 @@ export class CatalogSyncWorker {
}
const database = await this.repository.get(previous.databaseId);
if (!database) return undefined;
if (previous.phase === "memory_cleanup") {
const release = this.operations.reserve(database.id);
try {
const queued = await this.repository.updateSyncRun(runId, { state: "queued", cancelRequested: false,
errorCode: null, errorMessage: null, finishedAt: null, leaseOwner: null, leaseExpiresAt: null });
this.reservations.set(runId, release);
this.launch(runId);
return queued;
} catch (error) { release(); throw error; }
}
return await this.start(database, previous.scope, previous.tableIds);
}
@@ -179,6 +194,10 @@ export class CatalogSyncWorker {
if (!database || database.version !== claimed.requestedDatabaseVersion) {
throw new CatalogConflictError("Database binding changed before synchronization started");
}
if (claimed.phase === "memory_cleanup") {
await this.finishMemoryCleanup(database, claimed);
return;
}
const progress: CatalogSchemaScanProgress = async (phase, counts) => {
await this.checkCancelled(runId);
await this.repository.updateSyncRun(runId, {
@@ -230,36 +249,30 @@ export class CatalogSyncWorker {
claimed.scope,
claimed.tableIds,
snapshot,
runId,
);
if (!applied) throw new CatalogConflictError("Database binding changed before schema changes were applied");
await this.repository.updateSyncRun(runId, {
state: "succeeded",
phase: "completed",
counts: applied,
finishedAt: new Date().toISOString(),
observedSnapshot: null,
plannedDiff: null,
confirmationToken: null,
heartbeatAt: new Date().toISOString(),
leaseOwner: null,
leaseExpiresAt: null,
});
await this.repository.appendSyncEvent(runId, "info", "succeeded", "Synchronization completed.", { ...applied });
this.release(runId);
const cleanupRun = await this.repository.updateSyncRun(runId, { counts: applied });
if (!cleanupRun) throw new Error("Synchronization run disappeared");
await this.finishMemoryCleanup(database, cleanupRun);
/* Completion is recorded only after the durable Memory cleanup succeeds. */
return;
} catch (error) {
const current = await this.repository.getSyncRun(runId);
const cancelled = !timedOut && (error instanceof SyncCancelledError || controller.signal.aborted || current?.cancelRequested);
const failure = timedOut
const failure = current?.phase === "memory_cleanup"
? { code: "memory_cleanup_pending", message: "Catalog synchronized. Memory cleanup is pending; retry this synchronization to complete it." }
: timedOut
? { code: "schema_sync_timed_out", message: "Schema synchronization timed out." }
: safeFailure(error);
await this.repository.updateSyncRun(runId, {
state: cancelled ? "cancelled" : "failed",
phase: "completed",
phase: current?.phase === "memory_cleanup" ? "memory_cleanup" : "completed",
errorCode: cancelled ? null : failure.code,
errorMessage: cancelled ? null : failure.message,
finishedAt: new Date().toISOString(),
observedSnapshot: null,
plannedDiff: null,
observedSnapshot: current?.phase === "memory_cleanup" ? current.observedSnapshot : null,
plannedDiff: current?.phase === "memory_cleanup" ? current.plannedDiff : null,
confirmationToken: null,
leaseOwner: null,
leaseExpiresAt: null,
@@ -278,6 +291,20 @@ export class CatalogSyncWorker {
}
}
private async finishMemoryCleanup(database: WorkspaceDatabase, run: CatalogSyncRun): Promise<void> {
if (!run.plannedDiff || run.phase !== "memory_cleanup") throw new Error("Missing committed cleanup context");
await this.repository.appendSyncEvent(run.id, "info", "memory_cleanup", "Removing Memory cards with deleted physical dependencies.");
const memoryDeleted = await this.cleanupMemory(database, run);
const counts = { ...run.counts, memoryDeleted };
await this.repository.updateSyncRun(run.id, {
state: "succeeded", phase: "completed", counts, finishedAt: new Date().toISOString(),
observedSnapshot: null, plannedDiff: null, confirmationToken: null,
heartbeatAt: new Date().toISOString(), leaseOwner: null, leaseExpiresAt: null,
});
await this.repository.appendSyncEvent(run.id, "info", "succeeded", "Synchronization completed.", counts);
this.release(run.id);
}
private assertCapability(scope: CatalogSyncScope, snapshot: ObservedSchemaSnapshot): void {
const required = scope === "all" ? ["tables", "columns", "relationships"] as const : [scope] as const;
for (const name of required) {
+3 -1
View File
@@ -366,7 +366,7 @@ export type CatalogSyncState =
export type CatalogSyncPhase =
| "queued" | "connecting" | "scanning_tables" | "scanning_columns"
| "scanning_relationships" | "planning" | "awaiting_confirmation"
| "applying" | "completed";
| "applying" | "memory_cleanup" | "completed";
export interface CatalogSchemaDiff {
deletedTables: string[];
@@ -375,6 +375,7 @@ export interface CatalogSchemaDiff {
}
export interface CatalogSyncCounts {
memoryDeleted?: number;
tables?: number;
columns?: number;
relationships?: number;
@@ -587,6 +588,7 @@ export interface CatalogRepository {
scope: CatalogSyncScope,
tableIds: readonly string[],
snapshot: ObservedSchemaSnapshot,
syncRunId?: string,
): Promise<CatalogSyncCounts | undefined>;
createSyncRun(
databaseId: string,
+5
View File
@@ -30,8 +30,11 @@ export interface AppConfig {
dataRoot?: string;
ollamaEnsureTimeoutMs: number;
piManagementTimeoutMs: number;
/** Host CLI platform projected into Docker; not the browser or container OS. */
hostPlatform?: string;
secretsFile?: string;
installationConfigFile?: string;
evidenceHostRegistryRoot?: string;
modelCatalogFile?: string;
sensitivityNer?: {
pythonExecutable: string;
@@ -474,8 +477,10 @@ export function loadConfig(
dataRoot: env.THT_DATA_ROOT,
ollamaEnsureTimeoutMs: Number(env.OLLAMA_ENSURE_TIMEOUT_MS ?? 60000),
piManagementTimeoutMs: piManagementTimeout(env.PI_MANAGEMENT_TIMEOUT_MS),
hostPlatform: env.THT_HOST_PLATFORM,
secretsFile,
installationConfigFile,
evidenceHostRegistryRoot: env.THT_EVIDENCE_HOST_REGISTRY_ROOT || undefined,
modelCatalogFile,
sensitivityNer,
piAuthFile,
+14 -16
View File
@@ -47,9 +47,8 @@ const runtimeModelSchema = z.object({
}).strict();
const catalogSchema = z.object({
schemaVersion: z.literal(1),
defaultSession: canonicalId,
defaultMetadataGeneration: canonicalId.optional(),
schemaVersion: z.literal(2),
defaultInteraction: canonicalId,
embedding: z.object({ id: canonicalId, dimensions: z.number().int().positive() }).strict(),
models: z.array(runtimeModelSchema).max(64),
}).strict();
@@ -57,8 +56,7 @@ const catalogSchema = z.object({
export type RuntimeModel = z.infer<typeof runtimeModelSchema>;
export interface RuntimeModelCatalog {
readonly defaultSession: string | null;
readonly defaultMetadataGeneration: string | null;
readonly defaultInteraction: string | null;
readonly embedding: Readonly<{ id: string; dimensions: number }> | null;
sessionModels(): readonly RuntimeModel[];
metadataModels(): readonly RuntimeModel[];
@@ -66,19 +64,21 @@ export interface RuntimeModelCatalog {
}
class RestartLoadedRuntimeModelCatalog implements RuntimeModelCatalog {
readonly defaultSession: string | null;
readonly defaultMetadataGeneration: string | null;
readonly defaultInteraction: string | null;
readonly embedding: Readonly<{ id: string; dimensions: number }> | null;
readonly #sessions: readonly RuntimeModel[];
readonly #metadata: readonly RuntimeModel[];
readonly #sessionIds: ReadonlySet<string>;
constructor(catalog?: z.infer<typeof catalogSchema>) {
this.defaultSession = catalog?.defaultSession ?? null;
this.defaultMetadataGeneration = catalog?.defaultMetadataGeneration ?? null;
this.defaultInteraction = catalog?.defaultInteraction ?? null;
this.embedding = catalog ? Object.freeze({ ...catalog.embedding }) : null;
this.#sessions = Object.freeze((catalog?.models ?? []).filter((model) => model.session !== undefined));
this.#metadata = Object.freeze((catalog?.models ?? []).filter((model) => model.metadataGeneration !== undefined));
// One operational list. Session-only installations can still run Core, but once Admin
// LLM models are configured every selectable model must support both adapters.
const hasMetadata = catalog?.models.some((model) => model.metadataGeneration !== undefined);
this.#sessions = Object.freeze((catalog?.models ?? []).filter((model) =>
model.session !== undefined && (!hasMetadata || model.metadataGeneration !== undefined)));
this.#metadata = Object.freeze(this.#sessions.filter((model) => model.metadataGeneration !== undefined));
this.#sessionIds = new Set(this.#sessions.map((model) => model.id));
}
@@ -129,11 +129,9 @@ export function loadRuntimeModelCatalog(file?: string): RuntimeModelCatalog {
if (ids.size !== parsed.data.models.length) throw new Error("runtime model catalog contains duplicate models");
const sessions = parsed.data.models.filter((model) => model.session !== undefined).map((model) => model.id);
const metadata = parsed.data.models.filter((model) => model.metadataGeneration !== undefined).map((model) => model.id);
if (!sessions.includes(parsed.data.defaultSession)) throw new Error("runtime model catalog session default is invalid");
if ((metadata.length > 0) !== (parsed.data.defaultMetadataGeneration !== undefined)
|| (parsed.data.defaultMetadataGeneration !== undefined
&& !metadata.includes(parsed.data.defaultMetadataGeneration))) {
throw new Error("runtime model catalog metadata default is invalid");
if (!sessions.includes(parsed.data.defaultInteraction)
|| (metadata.length > 0 && !metadata.includes(parsed.data.defaultInteraction))) {
throw new Error("runtime model catalog interaction default is invalid");
}
return new RestartLoadedRuntimeModelCatalog(parsed.data);
}
+43 -21
View File
@@ -16,6 +16,8 @@ import { loadSettings } from "./settings/settings-store.js";
import { ThtRunner, type SessionRow } from "./tht/tht-runner.js";
import { WorkspaceRegistry } from "./workspaces/registry.js";
import { WorkspaceSecretStore } from "./workspaces/secret-store.js";
import { createCatalogRepository } from "./catalog/repository.js";
import type { CatalogRepository } from "./catalog/types.js";
type OperatorAction = "maintenance-activate" | "maintenance-deactivate" | "maintenance-status"
| "session-inventory" | "workflow-doctor" | "workspace-integrity"
@@ -30,7 +32,7 @@ const lifecyclePrincipal: PrincipalContext = {
isAdmin: true,
};
function operatorRunner(config: AppConfig): ThtRunner {
function operatorRunner(config: AppConfig, catalogRepository: CatalogRepository): ThtRunner {
const workspaceSecretStore = new WorkspaceSecretStore({
root: config.workspaceSecretStoreRoot,
runtimeRoot: config.workspaceSecretRuntimeRoot,
@@ -46,6 +48,7 @@ function operatorRunner(config: AppConfig): ThtRunner {
secretsFile: config.secretsFile,
secretFiles: config.secretFiles,
workspaceSecretStore,
catalogRepository,
semanticRuntime: {
internalQdrantUrl: config.internalQdrantUrl,
internalEmbeddingUrl: config.internalEmbeddingUrl,
@@ -55,36 +58,55 @@ function operatorRunner(config: AppConfig): ThtRunner {
}).withPrincipal(lifecyclePrincipal);
}
async function withOperatorRunner<T>(
config: AppConfig,
operation: (runner: ThtRunner) => Promise<T>,
): Promise<T> {
const catalogRepository = createCatalogRepository(config.catalogDatabase);
try {
return await operation(operatorRunner(config, catalogRepository));
} finally {
await catalogRepository.close?.();
}
}
async function sessionInventory(config: AppConfig): Promise<Array<Pick<SessionRow, "status" | "archived">>> {
const registry = new WorkspaceRegistry(config.workspaceRegistry);
const revisions = await registry.listRetainedSnapshots();
const runner = operatorRunner(config);
const sessions = new Map<string, SessionRow>();
for (const revision of revisions) {
for (const session of await runner.sessionList(revision.snapshotPath)) sessions.set(session.id, session);
}
return [...sessions.values()].map(({ status, archived }) => ({ status, archived: archived === true }));
return await withOperatorRunner(config, async (runner) => {
const sessions = new Map<string, SessionRow>();
for (const revision of revisions) {
for (const session of await runner.sessionList(revision.snapshotPath)) sessions.set(session.id, session);
}
return [...sessions.values()].map(({ status, archived }) => ({ status, archived: archived === true }));
});
}
async function workflowDiagnostics(config: AppConfig): Promise<{ ready: true; workspaces: number }> {
const registry = new WorkspaceRegistry(config.workspaceRegistry);
const revisions = await registry.listRetainedSnapshots();
if (revisions.length === 0) throw new Error("workflow diagnostics unavailable");
const runner = operatorRunner(config);
for (const revision of revisions) {
const result = await runner.run(["doctor", "--json"], revision.snapshotPath);
let payload: unknown;
try {
payload = JSON.parse(result.stdout);
} catch {
throw new Error("workflow diagnostics failed");
return await withOperatorRunner(config, async (runner) => {
for (const revision of revisions) {
const runtime = await runner.acquireWorkspaceRuntime(revision.snapshotPath);
try {
const result = await runner.run(["doctor", "--json"], runtime.path);
let payload: unknown;
try {
payload = JSON.parse(result.stdout);
} catch {
throw new Error("workflow diagnostics failed");
}
if (
result.code !== 0 || !payload || typeof payload !== "object"
|| (payload as { ok?: unknown }).ok !== true
) throw new Error("workflow diagnostics failed");
} finally {
runtime.release();
}
}
if (
result.code !== 0 || !payload || typeof payload !== "object"
|| (payload as { ok?: unknown }).ok !== true
) throw new Error("workflow diagnostics failed");
}
return { ready: true, workspaces: revisions.length };
return { ready: true, workspaces: revisions.length };
});
}
async function workspaceIntegrity(config: AppConfig): Promise<{
+9
View File
@@ -11,6 +11,7 @@ import {
validateDeclarativePiConfig,
} from "./managed-config.js";
import type { RuntimeModelCatalog } from "../models/runtime-model-catalog.js";
import { secretValue } from "../config/secret-bundle.js";
export interface PiModel {
provider: string;
@@ -64,6 +65,14 @@ export function createPiModelLister(cfg: AppConfig, opts: Opts = {}): ListModels
}
const env = buildPiChildEnv({});
// Pi's availability enumeration also needs the catalog-owned credentials for built-in
// providers. It must keep working after their obsolete Pi auth entries are removed.
for (const model of opts.modelCatalog?.sessionModels() ?? []) {
const name = model.authentication.mode === "secret_env" ? model.authentication.apiKeyEnv : undefined;
if (!name) continue;
const value = secretValue(cfg, name);
if (value) env[name] = value;
}
delete env.THT_DATA_ROOT;
if (cfg.dataRoot !== undefined) env.THT_DATA_ROOT = cfg.dataRoot;
const child = spawnFn(cfg.piBin, ["--mode", "rpc"], { cwd: cfg.harnessDir, env });
+13 -2
View File
@@ -138,7 +138,7 @@ export interface PiRuntimeAgentSnapshot {
* Bind a session Pi process to the exact managed auth/model bytes validated at spawn time.
* Other agent resources remain live through symlinks, while session storage stays persistent.
*/
export function createPiRuntimeAgentSnapshot(): PiRuntimeAgentSnapshot {
export function createPiRuntimeAgentSnapshot(options: { excludeAuthProvider?: string } = {}): PiRuntimeAgentSnapshot {
const sourceAgentDir = configuredPiAgentDir();
const auth = readPiAgentFile(sourceAgentDir, "auth.json", true);
const models = readPiAgentFile(sourceAgentDir, "models.json", true);
@@ -165,7 +165,18 @@ export function createPiRuntimeAgentSnapshot(): PiRuntimeAgentSnapshot {
);
}
if (auth !== undefined) {
writeFileSync(join(snapshotDir, "auth.json"), auth, { flag: "wx", mode: 0o600 });
let effectiveAuth = auth;
if (options.excludeAuthProvider) {
const parsed = parsePiConfigJson(auth);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new PiManagedConfigError();
const provider = options.excludeAuthProvider.trim().toLowerCase();
effectiveAuth = JSON.stringify(Object.fromEntries(
Object.entries(parsed).filter(([key]) => key.trim().toLowerCase() !== provider),
));
}
// secret_env is authoritative for this provider. Keep the operator's auth file intact,
// but do not let an old Pi credential override the shared bundle inside this child.
writeFileSync(join(snapshotDir, "auth.json"), effectiveAuth, { flag: "wx", mode: 0o600 });
}
if (models !== undefined) {
writeFileSync(join(snapshotDir, "models.json"), models, { flag: "wx", mode: 0o600 });
+14 -10
View File
@@ -36,6 +36,7 @@ export interface PiInstallationConfig {
}
export interface PiStatus {
hostPlatform: "linux" | "macos" | "windows";
version?: string;
ready: boolean;
credentials: PiCredentialStatus;
@@ -92,6 +93,9 @@ interface PiManagementDeps {
}
export function createPiManagement(config: AppConfig, deps: PiManagementDeps): PiManagementService {
const platform = config.hostPlatform ?? process.platform;
const hostPlatform = platform === "darwin" ? "macos"
: platform === "windows" || platform === "win32" ? "windows" : "linux";
const now = deps.now ?? (() => new Date());
const diagnostics: string[] = [];
const addDiagnostic = (message: string): void => {
@@ -106,23 +110,23 @@ export function createPiManagement(config: AppConfig, deps: PiManagementDeps): P
});
const credentialStatus = deps.credentialStatus ?? ((provider: string | undefined) => {
try {
const model = deps.modelCatalog.defaultSession
? deps.modelCatalog.sessionModels().find((entry) => entry.id === deps.modelCatalog.defaultSession)
const model = deps.modelCatalog.defaultInteraction
? deps.modelCatalog.sessionModels().find((entry) => entry.id === deps.modelCatalog.defaultInteraction)
: undefined;
const credentialName = model?.authentication.mode === "secret_env"
? model.authentication.apiKeyEnv
: undefined;
const configuredApiKey = configuredPiProviderApiKey(
const configuredApiKey = credentialName ? `$${credentialName}` : configuredPiProviderApiKey(
readConfiguredPiAgentFile("models.json", true),
provider,
) ?? (credentialName ? `$${credentialName}` : undefined);
);
return piProviderCredentialStatus({
provider,
authProviders: loadPiAuthProviders(),
authProviders: credentialName ? new Set() : loadPiAuthProviders(),
resolveCredentialValue: () => credentialName
? secretValue(config, credentialName)
: config.modelCatalogFile ? undefined : secretValue(config, "THT_MODEL_API_KEY"),
credentialFile: config.modelApiKeyFile,
credentialFile: credentialName ? undefined : config.modelApiKeyFile,
configuredApiKey,
});
} catch {
@@ -152,8 +156,8 @@ export function createPiManagement(config: AppConfig, deps: PiManagementDeps): P
const installationConfig = (): PiInstallationConfig => {
const settings = readSettings();
const reasoning = config.defaults.thinking ?? settings.thinking;
const selected = deps.modelCatalog.defaultSession
? splitCanonicalModelId(deps.modelCatalog.defaultSession)
const selected = deps.modelCatalog.defaultInteraction
? splitCanonicalModelId(deps.modelCatalog.defaultInteraction)
: undefined;
return {
...(selected ? selected : {}),
@@ -169,11 +173,11 @@ export function createPiManagement(config: AppConfig, deps: PiManagementDeps): P
try {
const currentVersion = await version();
addDiagnostic("Pi version probe succeeded");
return { version: currentVersion, ready: true, credentials, config: current, checkedAt };
return { hostPlatform, version: currentVersion, ready: true, credentials, config: current, checkedAt };
} catch (error) {
const message = stableMessage(error, "Pi runtime is unavailable");
addDiagnostic(message);
return { ready: false, credentials, config: current, checkedAt, message };
return { hostPlatform, ready: false, credentials, config: current, checkedAt, message };
}
},
+11 -13
View File
@@ -63,7 +63,7 @@ export class PiProcessManager {
) {
this.modelCatalog = opts?.modelCatalog ?? loadRuntimeModelCatalog(cfg.modelCatalogFile);
this.modelCatalogConfigured = cfg.modelCatalogFile !== undefined
|| this.modelCatalog.defaultSession !== null;
|| this.modelCatalog.defaultInteraction !== null;
this.loadAuthProviders = opts?.authProviders
?? ((agentDir) => loadPiAuthProviders({ agentDir }));
if (opts?.spawnFn) {
@@ -89,25 +89,23 @@ export class PiProcessManager {
// This is the final shared boundary for createFor(), spawnFor(), and resume(). Validate
// before auth-provider inspection, then make Pi consume the exact copied bytes rather than
// reopening mutable mounted auth/models files after this check.
const agent = createPiRuntimeAgentSnapshot();
const catalogModel = provider && model
? this.modelCatalog.sessionModels().find((entry) => entry.provider === provider && entry.model === model)
: undefined;
const credentialName = catalogModel?.authentication.mode === "secret_env"
? catalogModel.authentication.apiKeyEnv : undefined;
const agent = createPiRuntimeAgentSnapshot({ excludeAuthProvider: credentialName ? provider : undefined });
let child: ChildProcessWithoutNullStreams | undefined;
try {
const catalogModel = provider && model
? this.modelCatalog.sessionModels()
.find((entry) => entry.provider === provider && entry.model === model)
: undefined;
const credentialName = catalogModel?.authentication.mode === "secret_env"
? catalogModel.authentication.apiKeyEnv
: undefined;
const projectedApiKey = configuredPiProviderApiKey(agent.models, provider)
?? (credentialName ? `$${credentialName}` : undefined);
const projectedApiKey = credentialName ? `$${credentialName}`
: configuredPiProviderApiKey(agent.models, provider);
const env = buildPiChildEnv({
provider,
authProviders: this.loadAuthProviders(agent.agentDir),
authProviders: credentialName ? new Set() : this.loadAuthProviders(agent.agentDir),
credentialValue: credentialName
? secretValue(this.cfg, credentialName)
: this.modelCatalogConfigured ? undefined : secretValue(this.cfg, "THT_MODEL_API_KEY"),
credentialFile: this.cfg.modelApiKeyFile,
credentialFile: credentialName ? undefined : this.cfg.modelApiKeyFile,
configuredApiKey: projectedApiKey,
additions: { THT_SESSION: sessionId, THT_AUTHOR: author },
});
+6 -5
View File
@@ -70,28 +70,29 @@ export function createPiProviderSmoke(
try {
const canonicalProvider = canonicalPiProvider(provider);
if (!canonicalProvider || timeoutMs <= 0) throw providerFailure();
const configuredAuthProviders = authProviders();
const configuredAuthProviders = new Set(authProviders());
const configuredModels = options.readModelsStore
? options.readModelsStore()
: readConfiguredPiAgentFile("models.json", true);
const catalog = options.modelCatalog ?? loadRuntimeModelCatalog(config.modelCatalogFile);
const catalogConfigured = config.modelCatalogFile !== undefined
|| catalog.defaultSession !== null;
|| catalog.defaultInteraction !== null;
const catalogModel = catalog.sessionModels()
.find((entry) => entry.provider === canonicalProvider && entry.model === model);
const upstreamModel = catalogModel?.upstreamModel ?? model;
const credentialName = catalogModel?.authentication.mode === "secret_env"
? catalogModel.authentication.apiKeyEnv
: undefined;
const projectedApiKey = configuredPiProviderApiKey(configuredModels, canonicalProvider)
?? (credentialName ? `$${credentialName}` : undefined);
if (credentialName) configuredAuthProviders.delete(canonicalProvider);
const projectedApiKey = credentialName ? `$${credentialName}`
: configuredPiProviderApiKey(configuredModels, canonicalProvider);
const env = buildPiChildEnv({
provider: canonicalProvider,
authProviders: configuredAuthProviders,
credentialValue: credentialName
? secretValue(config, credentialName)
: catalogConfigured ? undefined : secretValue(config, "THT_MODEL_API_KEY"),
credentialFile: config.modelApiKeyFile,
credentialFile: credentialName ? undefined : config.modelApiKeyFile,
configuredApiKey: projectedApiKey,
});
clearPrincipalEnvironment(env);
+90
View File
@@ -0,0 +1,90 @@
import path from "node:path";
import type { FastifyInstance } from "fastify";
import { z } from "zod";
import { requirePermission, isPrincipalContext } from "../auth/authorization.js";
import type { ThtRunner } from "../tht/tht-runner.js";
import type { WorkspaceRegistry } from "../workspaces/registry.js";
import type { WorkspacePreprocessingService } from "../workspaces/preprocessing-service.js";
const params = z.object({ workspaceId: z.string().regex(/^[a-z][a-z0-9-]{2,62}$/),
evidenceId: z.string().regex(/^evidence:[a-z0-9]+(?:-[a-z0-9]+)*$/).optional() });
const query = z.object({
q: z.string().max(1000).optional(), kind: z.enum(["domain", "glossary", "enum", "example", "mapping", "normalization", "formula", "reference"]).optional(),
purpose: z.enum(["disambiguation", "rewriting", "schema_linking", "sql_generation"]).optional(),
status: z.enum(["new", "modified", "active", "removed", "review_required", "legacy", "invalid"]).optional(),
concept: z.string().max(300).optional(), table: z.string().max(300).optional(), column: z.string().max(300).optional(),
source: z.string().max(300).optional(), language: z.string().max(30).optional(),
sort: z.enum(["title", "id", "kind", "status"]).optional(), direction: z.enum(["asc", "desc"]).optional(),
page: z.coerce.number().int().positive().optional(), page_size: z.coerce.number().int().min(1).max(100).optional(),
}).strict();
const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
export function evidenceRoutes(app: FastifyInstance, deps: {
runner: Pick<ThtRunner, "withPrincipal">; registry: Pick<WorkspaceRegistry, "list">;
registryRoot: string; hostRegistryRoot?: string;
service: Partial<Pick<WorkspacePreprocessingService, "consolidateEvidence" | "evidenceSources">>;
}) {
app.post("/workspaces/:workspaceId/evidence/sources", async (request, reply) => {
const principal = requirePermission(request, reply, "evidence.manage");
if (!isPrincipalContext(principal)) return reply;
try {
const { workspaceId } = params.parse(request.params);
const body = z.discriminatedUnion("action", [
z.object({ action: z.literal("refresh") }).strict(),
z.object({ action: z.literal("decide"), sourceId: z.string().regex(/^[a-f0-9]{64}$/),
revision: z.string().regex(/^[a-f0-9]{64}$/), decision: z.enum(["keep", "replace"]) }).strict(),
]).parse(request.body);
if (!(await deps.registry.list()).some(w => w.id === workspaceId)) return reply.code(404).send({ code: "workspace_invalid" });
if (!deps.service.evidenceSources) throw new Error("Evidence service unavailable");
return await deps.service.evidenceSources({ workspaceId, ...body, actor: principal.subject });
} catch (error) {
return reply.code(error instanceof z.ZodError ? 400 : 503).send({ code: "evidence_unavailable",
message: error instanceof z.ZodError ? "Invalid source request." : "Evidence source service is unavailable." });
}
});
app.get<{ Params: { workspaceId: string; evidenceId?: string } }>("/workspaces/:workspaceId/evidence", read);
app.get<{ Params: { workspaceId: string; evidenceId?: string } }>("/workspaces/:workspaceId/evidence/:evidenceId", read);
async function read(request: import("fastify").FastifyRequest, reply: import("fastify").FastifyReply) {
const principal = requirePermission(request, reply, "evidence.manage");
if (!isPrincipalContext(principal)) return reply;
try {
const { workspaceId, evidenceId } = params.parse(request.params);
const filters = query.parse(request.query);
if (!(await deps.registry.list()).some(w => w.id === workspaceId)) return reply.code(404).send({ code: "workspace_invalid" });
const root = path.join(deps.registryRoot, "repo", workspaceId);
const result = await deps.runner.withPrincipal(principal).runWithRuntimeSnapshot(
["evidence", "admin", "--workspace", workspaceId],
JSON.stringify({ root, query: { ...filters, ...(evidenceId ? { id: evidenceId } : {}) } }),
);
const payload = JSON.parse(result.stdout);
if (result.code !== 0) return reply.code(503).send(payload);
if (evidenceId && !payload.item) return reply.code(404).send({ code: "evidence_not_found", message: "Evidence was not found." });
const host = deps.hostRegistryRoot;
const hostPath = host && (path.isAbsolute(host) || path.win32.isAbsolute(host))
? (path.win32.isAbsolute(host) && !path.isAbsolute(host) ? path.win32 : path).join(host, "repo") : null;
const hostJoin = hostPath && path.win32.isAbsolute(hostPath) && !path.isAbsolute(hostPath) ? path.win32.join : path.join;
return { ...payload, location: { repository: hostPath, workspace: hostPath ? hostJoin(hostPath, workspaceId) : null,
runtime_workspace: root, host: "Installation host", command: `tht workspace evidence consolidate --workspace ${workspaceId}`,
git_commands: hostPath ? [
`git -C ${quote(hostPath)} status --short -- ${quote(workspaceId + "/evidence")}`,
`git -C ${quote(hostPath)} diff -- ${quote(workspaceId + "/evidence")}`,
`git -C ${quote(hostPath)} add -A -- ${quote(workspaceId + "/evidence")}`,
`git -C ${quote(hostPath)} commit --only -m 'Curate Evidence' -- ${quote(workspaceId + "/evidence")}`,
`git -C ${quote(hostPath)} push`,
] : [] } };
} catch (error) {
return reply.code(error instanceof z.ZodError ? 400 : 503).send({ code: "evidence_unavailable",
message: error instanceof z.ZodError ? "Evidence filters are invalid." : "Evidence archive is unavailable." });
}
}
app.post("/workspaces/:workspaceId/evidence/consolidate", async (request, reply) => {
const principal = requirePermission(request, reply, "evidence.manage");
if (!isPrincipalContext(principal)) return reply;
try {
const { workspaceId } = params.parse(request.params);
if (!(await deps.registry.list()).some(w => w.id === workspaceId)) return reply.code(404).send({ code: "workspace_invalid" });
if (!deps.service.consolidateEvidence) throw new Error("Evidence service unavailable");
return await deps.service.consolidateEvidence({ workspaceId });
} catch { return reply.code(503).send({ code: "evidence_unavailable", message: "Evidence consolidation is unavailable." }); }
});
}
+94
View File
@@ -0,0 +1,94 @@
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod";
import { isPrincipalContext, requirePermission } from "../auth/authorization.js";
import type { ThtRunner } from "../tht/tht-runner.js";
import type { WorkspaceRegistry } from "../workspaces/registry.js";
import type { SemanticRuntimeConfig } from "../workspaces/runtime-renderer.js";
const paramsSchema = z.object({
workspaceId: z.string().regex(/^[a-z][a-z0-9_-]{0,63}$/),
cardId: z.string().regex(/^mem-[0-9a-f-]{36}$/).optional(),
});
const family = z.enum(["domain_clarification", "sql_rule", "solved_question", "explained_error"]);
const cardSchema = z.object({
family, subject: z.string().trim().min(1).max(1000),
detail: z.string().max(50000).default(""), scope: z.string().trim().min(1).max(10000),
rationale: z.string().max(10000).default(""), question: z.string().max(10000).default(""),
sql: z.string().max(100000).default(""),
concepts: z.array(z.string().trim().min(1).max(200)).max(100).default([]),
dependencies: z.array(z.object({
database: z.string().trim().min(1).max(200), schema_name: z.string().max(200).default(""),
table: z.string().max(200).default(""), column: z.string().max(200).default(""),
}).strict()).max(200).default([]),
links: z.array(z.object({
target_id: z.string().min(1).max(100), meaning: z.string().trim().min(1).max(1000),
}).strict()).max(200).default([]),
}).strict();
const querySchema = z.object({
q: z.string().max(1000).optional(), family: family.optional(),
concept: z.string().max(200).optional(), database: z.string().max(200).optional(),
table: z.string().max(200).optional(), column: z.string().max(200).optional(),
origin: z.enum(["manual", "workflow"]).optional(),
updated_after: z.iso.datetime({ offset: true }).optional(),
updated_before: z.iso.datetime({ offset: true }).optional(),
page: z.coerce.number().int().positive().optional(),
page_size: z.coerce.number().int().min(1).max(100).optional(),
sort: z.enum(["updated_at", "created_at", "subject", "family"]).optional(),
direction: z.enum(["asc", "desc"]).optional(),
}).strict();
export function memoryRoutes(app: FastifyInstance, deps: {
runner: Pick<ThtRunner, "withPrincipal">;
registry: Pick<WorkspaceRegistry, "list" | "read">;
runtime: SemanticRuntimeConfig;
}) {
const invoke = (action: string) => async (request: FastifyRequest, reply: FastifyReply) => {
const principal = requirePermission(request, reply, "memory.manage");
if (!isPrincipalContext(principal)) return reply;
try {
const { workspaceId, cardId } = paramsSchema.parse(request.params);
const input = action === "list" ? querySchema.parse(request.query)
: action === "create" || action === "update"
? { card: cardSchema.parse(request.body), ...(cardId ? { id: cardId } : {}) }
: cardId ? { id: cardId } : {};
// Registry identity is enough: administrative browsing must not require a DWH binding.
const workspaces = await deps.registry.list();
if (!workspaces.some(workspace => workspace.id === workspaceId)) {
return reply.code(404).send({ code: "workspace_invalid", message: "Workspace was not found." });
}
const { workspace } = await deps.registry.read(workspaceId);
const result = await deps.runner.withPrincipal(principal).runWithRuntimeSnapshot(
["memory", "admin", "--workspace", workspaceId],
JSON.stringify({ action, request: input, runtime: {
...deps.runtime, memoryLanguage: workspace.workspace.language,
} }),
);
let payload;
try { payload = JSON.parse(result.stdout); } catch {
return reply.code(503).send({ code: "memory_unavailable", message: "Memory archive is unavailable." });
}
if (result.code !== 0) {
const codes: Record<string, number> = {
memory_invalid: 400, memory_forbidden: 403, memory_not_found: 404,
memory_conflict: 409, memory_unavailable: 503,
};
const code = typeof payload?.code === "string" && payload.code in codes
? payload.code : "memory_unavailable";
return reply.code(codes[code]).send({ code, message: "Memory operation could not be completed." });
}
return reply.code(action === "create" ? 201 : 200).send(payload);
} catch (error) {
if (error instanceof z.ZodError) {
return reply.code(400).send({ code: "memory_invalid", message: "Memory request is invalid." });
}
return reply.code(503).send({ code: "memory_unavailable", message: "Memory archive is unavailable." });
}
};
app.get("/workspaces/:workspaceId/memory", invoke("list"));
app.post("/workspaces/:workspaceId/memory", invoke("create"));
app.get("/workspaces/:workspaceId/memory/pending", invoke("pending"));
app.get("/workspaces/:workspaceId/memory/:cardId", invoke("show"));
app.put("/workspaces/:workspaceId/memory/:cardId", invoke("update"));
app.delete("/workspaces/:workspaceId/memory/:cardId", invoke("delete"));
app.post("/workspaces/:workspaceId/memory/:cardId/retry", invoke("retry"));
}
+68 -20
View File
@@ -150,13 +150,34 @@ export function sessionRoutes(
});
app.addHook("onResponse", async (req) => { admissionLeases.get(req)?.(); });
type SessionRevisionScan = {
revisions: Awaited<ReturnType<typeof d.workspaceRegistry.list>>;
retainedComplete: boolean;
};
let retainedSnapshotWarning: string | null = null;
/** Include retained historical descriptors so removed workspaces remain resumable. */
const sessionRevisions = async () => {
const sessionRevisions = async (): Promise<SessionRevisionScan> => {
const registry = d.workspaceRegistry as Partial<WorkspaceRegistry>;
if (typeof registry.listRetainedSnapshots === "function") {
return await registry.listRetainedSnapshots();
try {
const revisions = await registry.listRetainedSnapshots();
retainedSnapshotWarning = null;
return { revisions, retainedComplete: true };
} catch (error) {
// A legacy/corrupt historical snapshot must not make active sessions (and their SSE
// reviewer gates) unreachable. The registry still rejects that snapshot; this fallback
// exposes only descriptors from the verified active state and deliberately disables
// retention reconciliation because the resulting session view is incomplete.
const detail = error instanceof Error ? error.message : "unknown error";
if (retainedSnapshotWarning !== detail) {
console.warn("[sessions] retained snapshot discovery failed; using active snapshots:", detail);
retainedSnapshotWarning = detail;
}
return { revisions: await d.workspaceRegistry.list(), retainedComplete: false };
}
}
return await d.workspaceRegistry.list();
return { revisions: await d.workspaceRegistry.list(), retainedComplete: true };
};
const isNotFound = (error: unknown) =>
@@ -200,7 +221,7 @@ export function sessionRoutes(
};
let revisions: Awaited<ReturnType<typeof d.workspaceRegistry.list>>;
try {
revisions = await sessionRevisions();
({ revisions } = await sessionRevisions());
} catch (registryError) {
// Sessions created before revision pinning still live under the installation's legacy
// default config. Keep that compatibility path available when a fresh installation has
@@ -362,6 +383,10 @@ export function sessionRoutes(
question: string; name?: string; workspace?: string; workspaceId?: string;
provider?: string; model?: string; thinking?: string;
};
if ((b.provider === undefined) !== (b.model === undefined)
|| (b.provider !== undefined && (typeof b.provider !== "string" || typeof b.model !== "string" || !b.provider || !b.model))) {
return reply.code(400).send({ error: "provider and model must be supplied together" });
}
const principal = getPrincipal(req);
let s: Settings;
try { s = await d.getSettings(principal); } catch { return storageFailure(reply); }
@@ -425,11 +450,9 @@ export function sessionRoutes(
}
}
const requestedCanonical = b.provider && b.model ? `${b.provider}/${b.model}` : undefined;
let selectedCanonical = requestedCanonical ?? d.modelCatalog.defaultSession;
let modelWarning: string | undefined;
if (selectedCanonical && d.modelCatalog.defaultSession && !d.modelCatalog.hasSession(selectedCanonical)) {
selectedCanonical = d.modelCatalog.defaultSession;
modelWarning = `Configured model ${requestedCanonical ?? "selection"} is unavailable; using ${selectedCanonical}.`;
const selectedCanonical = requestedCanonical ?? d.modelCatalog.defaultInteraction;
if (selectedCanonical && d.modelCatalog.defaultInteraction && !d.modelCatalog.hasSession(selectedCanonical)) {
return reply.code(503).send({ error: MODEL_UNAVAILABLE_MESSAGE, code: "model_unavailable" });
}
const selected = selectedCanonical ? splitCanonicalModelId(selectedCanonical) : undefined;
const provider = selected?.provider ?? b.provider;
@@ -533,7 +556,7 @@ export function sessionRoutes(
),
() => d.mgr.start(id, rt, runtimeOptions),
);
return { id, ...(modelWarning ? { warning: modelWarning } : {}) };
return { id };
} finally {
if (revisionLease && !manifestPersisted) {
await revisionLease.abort().catch((error: unknown) => {
@@ -559,8 +582,8 @@ export function sessionRoutes(
? { ...principal, isAdmin: false }
: ownershipPrincipal(principal, "session.read_all");
const runner = runnerFor(scopedPrincipal);
const revisions = await sessionRevisions();
const lists = await Promise.all(revisions
const revisionScan = await sessionRevisions();
const lists = await Promise.all(revisionScan.revisions
.map((revision) => runner.sessionList(revision.snapshotPath) as Promise<SessionRow[]>));
const sessions = new Map<string, SessionRow>();
for (const row of lists.flat()) {
@@ -570,7 +593,8 @@ export function sessionRoutes(
// Only an administrator-visible complete list (or the single local principal) is safe
// input for retention. A remote per-user view can never discard another principal's pin.
const reconcileSnapshotRetention = (d.workspaceRegistry as Partial<WorkspaceRegistry>).reconcileSnapshotRetention;
const hasCompleteRetentionView = (scope === "all" || principal.issuer === "local")
const hasCompleteRetentionView = revisionScan.retainedComplete
&& (scope === "all" || principal.issuer === "local")
&& hasPermission(principal, "session.read_all");
if (hasCompleteRetentionView && typeof reconcileSnapshotRetention === "function") {
const retained = [...new Set(list
@@ -604,6 +628,23 @@ export function sessionRoutes(
} catch (error) { return lifecycleFailure(reply, error); }
const rt = d.mgr.get(id);
if (!rt) return reply.code(404).send({ error: "sessione non attiva" });
const pending = rt.bridge.pendingWidget?.() as any;
const response = (req.body as any)?.ui_response;
if (pending?.widget === "archive-repair" && response?.id === pending.id && !response.control) {
const choices = response.choices;
if (!Array.isArray(choices) || choices.length !== 1)
return reply.code(400).send({ error: "Select one archive repair choice" });
if (choices[0] !== "continue" && choices[0] !== "reject") {
const option = pending.repair?.options?.find((item: any) => item.id === choices[0]);
if (!option || !["memory", "evidence"].includes(option.archive))
return reply.code(400).send({ error: "Unknown archive repair choice" });
const permission = option.archive === "memory" ? "memory.manage" : "evidence.manage";
if (!principal.isAdmin || !hasPermission(principal, permission))
return reply.code(403).send({ error: "Archive corrections require an administrator" });
if (rt.ownerKey !== `${principal.issuer}\0${principal.subject}`)
return reply.code(409).send({ error: "Resume the session with your account before correcting an archive" });
}
}
if (!rt.bridge.respond((req.body as any).ui_response)) {
return reply.code(409).send({ error: "risposta non corrispondente al gate in attesa" });
}
@@ -641,10 +682,17 @@ export function sessionRoutes(
provider?: string; model?: string; thinking?: string;
workspace_id?: string; workspace_revision?: string;
};
const savedCanonical = saved.provider && saved.model ? `${saved.provider}/${saved.model}` : "";
if (d.modelCatalog.defaultSession && (!savedCanonical || !d.modelCatalog.hasSession(savedCanonical))) {
const requested = (req.body ?? {}) as { provider?: string; model?: string; thinking?: string };
if ((requested.provider === undefined) !== (requested.model === undefined)
|| (requested.provider !== undefined && (typeof requested.provider !== "string" || typeof requested.model !== "string" || !requested.provider || !requested.model))) {
return reply.code(400).send({ error: "provider and model must be supplied together" });
}
const selectedCanonical = requested.provider && requested.model
? `${requested.provider}/${requested.model}` : d.modelCatalog.defaultInteraction;
if (d.modelCatalog.defaultInteraction && (!selectedCanonical || !d.modelCatalog.hasSession(selectedCanonical))) {
return reply.code(503).send({ error: MODEL_UNAVAILABLE_MESSAGE, code: "model_unavailable" });
}
const selected = selectedCanonical ? splitCanonicalModelId(selectedCanonical) : saved;
let workspaceConfigPath: string;
let workspaceDescriptor: WorkspaceDescriptor | undefined;
try {
@@ -677,7 +725,7 @@ export function sessionRoutes(
if (existing) {
const state = existing.bridge.turnState();
if (state === "running" || state === "waiting") {
return reply.code(200).send({ id, alreadyActive: true });
return reply.code(200).send({ id, alreadyActive: true, workspaceId: saved.workspace_id });
}
}
const ensure = await d.readiness.ensure(
@@ -688,8 +736,8 @@ export function sessionRoutes(
...(ensure.code ? { code: ensure.code } : {}),
});
const options = {
provider: saved?.provider,
model: saved?.model,
provider: selected.provider,
model: selected.model,
thinking: saved?.thinking ?? settings.thinking,
author: principal.displayName ?? principal.subject,
principal,
@@ -711,7 +759,7 @@ export function sessionRoutes(
if (current) {
const state = current.bridge.turnState();
if (state === "running" || state === "waiting") {
return reply.code(200).send({ id, alreadyActive: true });
return reply.code(200).send({ id, alreadyActive: true, workspaceId: saved.workspace_id });
}
}
@@ -752,7 +800,7 @@ export function sessionRoutes(
d.mgr.configure(rt, runtimeOptions), null,
() => d.mgr.start(id, rt, runtimeOptions),
);
return reply.code(200).send({ id, alreadyActive: false });
return reply.code(200).send({ id, alreadyActive: false, workspaceId: saved.workspace_id });
});
});
app.post("/sessions/:id/close", async (req, reply) => {
+2 -2
View File
@@ -16,8 +16,8 @@ export function effectiveSettings(
modelCatalog?: RuntimeModelCatalog,
): Settings {
const workspaces = listWorkspaces(cfg.harnessDir);
const selected = modelCatalog?.defaultSession
? splitCanonicalModelId(modelCatalog.defaultSession)
const selected = modelCatalog?.defaultInteraction
? splitCanonicalModelId(modelCatalog.defaultInteraction)
: undefined;
return {
workspace: stored.workspace ?? workspaces[0]?.name,
+16 -2
View File
@@ -18,7 +18,7 @@ export interface WorkspaceMaintenanceIo {
writeStderr(value: string): void;
}
type Command = "inspect" | "preprocess-run" | "preprocess-clear";
type Command = "inspect" | "preprocess-run" | "preprocess-clear" | "evidence-consolidate" | "evidence-refresh" | "evidence-decide";
function failureResult(
operation: string,
@@ -65,6 +65,9 @@ function parseRequest(command: string, stdin: string): Record<string, unknown> {
inspect: ["schemaVersion", "workspaceId"],
"preprocess-run": ["schemaVersion", "workspaceId"],
"preprocess-clear": ["schemaVersion", "workspaceId"],
"evidence-consolidate": ["schemaVersion", "workspaceId"],
"evidence-refresh": ["schemaVersion", "workspaceId"],
"evidence-decide": ["schemaVersion", "workspaceId", "sourceId", "revision", "decision"],
};
const allowed = allowedByCommand[command];
if (!allowed) throw new Error("unknown command");
@@ -81,6 +84,16 @@ function exitCodeFor(result: WorkspaceOperationResult): number {
async function dispatch(command: Command, service: WorkspacePreprocessingService, request: Record<string, unknown>): Promise<WorkspaceOperationResult> {
switch (command) {
case "evidence-refresh":
return await service.evidenceSources({ workspaceId: request.workspaceId as string, action: "refresh" });
case "evidence-decide":
if (typeof request.sourceId !== "string" || !/^[a-f0-9]{64}$/.test(request.sourceId)
|| typeof request.revision !== "string" || !/^[a-f0-9]{64}$/.test(request.revision)
|| (request.decision !== "keep" && request.decision !== "replace")) throw new Error("invalid request");
return await service.evidenceSources({ workspaceId: request.workspaceId as string, action: "decide",
sourceId: request.sourceId, revision: request.revision, decision: request.decision });
case "evidence-consolidate":
return await service.consolidateEvidence({ workspaceId: request.workspaceId as string });
case "inspect":
return await service.inspect({ workspaceId: request.workspaceId as string });
case "preprocess-run":
@@ -127,7 +140,8 @@ export async function runWorkspaceMaintenanceCli(
|| message === "unexpected request field"
|| message === "invalid workspace id";
return command in {
inspect: true, "preprocess-run": true, "preprocess-clear": true,
inspect: true, "preprocess-run": true, "preprocess-clear": true, "evidence-consolidate": true,
"evidence-refresh": true, "evidence-decide": true,
} ? (requestError ? 2 : 1) : 2;
}
}
@@ -22,6 +22,7 @@ export interface EvidencePreprocessingRequest {
evidence: EvidenceConfig;
job: EvidenceJobState;
dryRun?: boolean;
consolidate?: boolean;
httpPrivateHostAllowlist?: readonly string[];
}
@@ -56,7 +57,7 @@ function isPrivateHost(hostname: string): boolean {
return hostname.endsWith(".internal");
}
function evidencePolicy(
export function evidencePolicy(
evidence: EvidenceConfig,
httpPrivateHostAllowlist?: readonly string[],
): EvidencePreprocessingOutcome | undefined {
@@ -95,13 +96,14 @@ function jobResult(job: EvidenceJobState): Pick<
};
}
async function runEvidenceStage(
export async function runEvidenceStage(
request: EvidencePreprocessingRequest,
deps: EvidencePreprocessingDependencies,
): Promise<EvidencePreprocessingOutcome> {
const payload = await deps.runStage([
"preprocess",
"evidence",
...(request.consolidate ? ["--consolidate"] : []),
...(request.dryRun ? ["--dry-run"] : []),
...(request.job.childRuns.evidence
? ["--resume", request.job.childRuns.evidence]
@@ -3,6 +3,8 @@ import { renameSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";
import {
continueEvidencePreprocessing,
evidencePolicy,
runEvidenceStage,
type EvidencePreprocessingDependencies,
type EvidencePreprocessingOutcome,
} from "./evidence/preprocessing.js";
@@ -108,6 +110,72 @@ function baseResult(
export class WorkspacePreprocessingService {
constructor(private readonly deps: WorkspacePreprocessingServiceDeps) {}
async evidenceSources(options: { workspaceId: string; action: "refresh" | "decide";
sourceId?: string; revision?: string; decision?: "keep" | "replace"; actor?: string }): Promise<WorkspaceOperationResult> {
const runtime = await this.deps.acquireActiveRuntime(options.workspaceId);
try {
if (options.action === "refresh") {
const refused = evidencePolicy(runtime.workspace.evidence, this.deps.httpPrivateHostAllowlist);
if (refused) return baseResult(runtime, "evidence refresh", "failed", refused.code);
}
const argv = ["evidence", "sources", options.action, "--json", "-c", "/dev/fd/3"];
if (options.action === "decide") {
if (!/^[a-f0-9]{64}$/.test(options.sourceId ?? "") || !/^[a-f0-9]{64}$/.test(options.revision ?? "")
|| !["keep", "replace"].includes(options.decision ?? "")) throw new Error("Invalid source decision");
const preflight = await this.deps.evidencePreflight(runtime.workspace);
if (!preflight.ok) return baseResult(runtime, "evidence sources", "failed", preflight.code);
argv.push("--source-id", options.sourceId!, "--revision", options.revision!, "--decision", options.decision!);
}
argv.push("--actor", options.actor ?? "installation operator");
const result = await this.deps.runChild({ argv, configPath: runtime.configLease.path });
const payload = JSON.parse(result.stdout);
if (result.exitCode !== 0 || payload.status !== "succeeded") throw new Error(
typeof payload.error === "string" ? payload.error.slice(0, 1500) : "Evidence source operation failed");
return baseResult(runtime, `evidence ${options.action}`, "succeeded", "ok", {
counts: this.numberRecord(payload.counts),
warnings: [options.action === "refresh" ? "Source comparisons are ready in Evidence management. Active content is unchanged."
: "Source decision activated locally. Commit and push the Evidence tree manually."],
});
} catch (error) {
return baseResult(runtime, `evidence ${options.action}`, "failed", "evidence_materialization_required", {
warnings: [error instanceof Error ? error.message : "Evidence source operation failed"],
});
}
}
async consolidateEvidence(options: { workspaceId: string }): Promise<WorkspaceOperationResult> {
const runtime = await this.deps.acquireActiveRuntime(options.workspaceId);
const preflight = await this.deps.evidencePreflight(runtime.workspace);
if (!preflight.ok) return baseResult(runtime, "evidence consolidate", "failed", preflight.code);
// Evidence has its own durable archive/corpus jobs. Do not alter Catalog readiness
// or the full preprocessing job when publishing this one component.
try {
const outcome = await runEvidenceStage({ evidence: runtime.workspace.evidence,
consolidate: true, job: { runId: randomBytes(16).toString("hex"), completedStages: [], childRuns: {} },
}, {
runStage: async argv => {
const result = await this.deps.runChild({ argv, configPath: runtime.configLease.path });
const payload = JSON.parse(result.stdout);
if (result.exitCode !== 0 || payload.status !== "succeeded") {
return Promise.reject(new Error(typeof payload.error === "string" ? payload.error.slice(0, 1500) : "Evidence consolidation failed. Retry the command."));
}
return payload;
},
persistJob: () => undefined,
evidencePreflight: async () => preflight,
requireRunId: value => this.requireRunId(value),
numberRecord: value => this.numberRecord(value),
});
return baseResult(runtime, "evidence consolidate", "succeeded", "ok", {
...outcome, warnings: ["Evidence is active locally. Catalog and Schema readiness are unchanged. Commit and push the Evidence files manually."],
});
} catch (error) {
return baseResult(runtime, "evidence consolidate", "failed", "evidence_materialization_required", {
warnings: [error instanceof Error ? error.message : "Evidence consolidation failed. Retry the command."],
});
}
}
async inspect(options: { workspaceId: string }): Promise<WorkspaceOperationResult> {
try {
const runtime = await this.deps.acquireActiveRuntime(options.workspaceId);
@@ -497,7 +497,10 @@ export async function publishDeterministicRuntimeConfigLease(options: {
rendered.workspaceRevision,
renderedConfigObject,
);
const identitySuffix = inputFingerprintValue.slice(7, 23);
// The Catalog input identity intentionally excludes representation-only changes.
// A lease also identifies its bytes, so a new renderer never collides with an
// immutable config produced by an earlier release for the same Catalog inputs.
const identitySuffix = sha256(inputFingerprintValue + "\n" + publishedConfig).slice(7, 23);
const preprocessingRoot = ensureTrustedDirectory(join(
options.dataRoot,
+2 -1
View File
@@ -1,4 +1,4 @@
import { basename, join } from "node:path";
import { basename, dirname, join } from "node:path";
import { stringify } from "yaml";
import { buildInstallationContract } from "./contracts.js";
import { validateWorkspaceDescriptor, type WorkspaceDescriptor } from "./schema.js";
@@ -179,6 +179,7 @@ function renderEvidence(
return {
evidence: {
...(workspace.evidence.schema_version === 2 ? { schema_version: 2 } : {}),
local_archive_root: join(dirname(dirname(context.revisionContentRoot)), "repo", context.workspaceId),
sources: [renderedSource],
},
vector: {
@@ -0,0 +1,48 @@
import Fastify from "fastify";
import { expect, test, vi } from "vitest";
import { sessionRoutes } from "../src/routes/sessions.js";
import type { PrincipalContext } from "../src/auth/principal.js";
test.each([
[false, "m", 403], [false, "e", 403], [false, "reject", 204],
[true, "m", 204], [true, "e", 204], [true, "forged", 400],
] as const)("archive repair response enforces the responding principal (%s, %s)", async (admin, choice, status) => {
const app = Fastify();
const principal: PrincipalContext = { issuer: "test", subject: "reviewer", isAdmin: admin,
roles: admin ? ["admin"] : ["user"],
permissions: admin ? ["session.use", "memory.manage", "evidence.manage"] : ["session.use"] };
app.addHook("preHandler", async request => { request.principal = principal; });
const respond = vi.fn(() => true);
sessionRoutes(app, {
tht: {}, mgr: { get: () => ({ ownerKey: "test\0reviewer", bridge: {
respond, pendingWidget: () => ({ id: "gate", widget: "archive-repair", repair: { options: [
{ id: "m", archive: "memory" }, { id: "e", archive: "evidence" },
] } }),
} }) },
} as unknown as Parameters<typeof sessionRoutes>[1]);
try {
const result = await app.inject({ method: "POST", url: "/sessions/s/response",
payload: { ui_response: { id: "gate", choices: [choice] } } });
expect(result.statusCode).toBe(status);
expect(respond).toHaveBeenCalledTimes(status === 204 ? 1 : 0);
} finally { await app.close(); }
});
test("an administrator cannot attribute a repair to another runtime's principal", async () => {
const app = Fastify();
app.addHook("preHandler", async request => { request.principal = {
issuer: "test", subject: "second-admin", isAdmin: true, roles: ["admin"],
permissions: ["session.use", "memory.manage"],
}; });
const respond = vi.fn();
sessionRoutes(app, { tht: {}, mgr: { get: () => ({ ownerKey: "test\0first-admin", bridge: {
respond, pendingWidget: () => ({ id: "gate", widget: "archive-repair",
repair: { options: [{ id: "m", archive: "memory" }] } }),
} }) } } as unknown as Parameters<typeof sessionRoutes>[1]);
try {
const result = await app.inject({ method: "POST", url: "/sessions/s/response",
payload: { ui_response: { id: "gate", choices: ["m"] } } });
expect(result.statusCode).toBe(409);
expect(respond).not.toHaveBeenCalled();
} finally { await app.close(); }
});
+2
View File
@@ -188,6 +188,8 @@ test("roles collapse duplicates and admin contains all administrative permission
"workspace.manage",
"workspace.secrets.manage",
"database.manage",
"memory.manage",
"evidence.manage",
"pi.manage",
"auth.diagnostics.read",
]);
+1 -1
View File
@@ -130,7 +130,7 @@ test("local login sets a non-persistent opaque session cookie and exposes only a
roles: ["admin"],
permissions: [
"session.use", "session.read_all", "session.manage_all", "settings.manage",
"workspace.manage", "workspace.secrets.manage", "database.manage", "pi.manage", "auth.diagnostics.read",
"workspace.manage", "workspace.secrets.manage", "database.manage", "memory.manage", "evidence.manage", "pi.manage", "auth.diagnostics.read",
],
isAdmin: true,
csrfToken: expect.stringMatching(/^[A-Za-z0-9_-]{43}$/),
+32 -6
View File
@@ -7,6 +7,8 @@ import { join } from "node:path";
import { expandLocalHome, localPrincipal, upstreamPrincipal } from "../src/auth/principal.js";
import { buildApp } from "../src/app.js";
import { loadConfig } from "../src/config.js";
import { rolesToPermissions } from "../src/auth/config.js";
import type { Permission, Role } from "../src/auth/types.js";
test("server smoke rejects retired trusted claims under OIDC authentication", () => {
const smoke = readFileSync("../scripts/unified-deployment-smoke.sh", "utf8");
@@ -32,7 +34,7 @@ test("local mode resolves a stable local principal", async () => {
roles: ["admin"],
permissions: [
"session.use", "session.read_all", "session.manage_all", "settings.manage",
"workspace.manage", "workspace.secrets.manage", "database.manage", "pi.manage", "auth.diagnostics.read",
"workspace.manage", "workspace.secrets.manage", "database.manage", "memory.manage", "evidence.manage", "pi.manage", "auth.diagnostics.read",
],
isAdmin: true,
});
@@ -74,7 +76,7 @@ test("upstream mode accepts only normalized proxy principal headers", async () =
issuer: "portal", subject: "42", displayName: "Alice", roles: ["user", "admin"],
permissions: [
"session.use", "session.read_all", "session.manage_all", "settings.manage",
"workspace.manage", "workspace.secrets.manage", "database.manage", "pi.manage", "auth.diagnostics.read",
"workspace.manage", "workspace.secrets.manage", "database.manage", "memory.manage", "evidence.manage", "pi.manage", "auth.diagnostics.read",
],
isAdmin: true,
});
@@ -206,15 +208,33 @@ test("the session boundary rejects tht maintenance headers outside exact loopbac
})).statusCode).toBe(503);
});
test("the session boundary touches a valid cookie session through the bounded Task 7 store operation", async () => {
test.each<{
name: string;
roles: Role[];
storedPermissions: Permission[];
}>([
{ name: "current user", roles: ["user"], storedPermissions: ["session.use"] },
{
name: "administrator signed in before archive permissions existed",
roles: ["admin"],
storedPermissions: rolesToPermissions(["admin"]).filter(
(permission) => permission !== "memory.manage" && permission !== "evidence.manage",
),
},
{
name: "user with obsolete administrative permissions",
roles: ["user"],
storedPermissions: ["session.use", "memory.manage", "evidence.manage"],
},
])("the session boundary derives current permissions and touches a valid cookie: $name", async ({ roles, storedPermissions }) => {
const sessions = {
resolve: vi.fn(async () => ({
version: 1,
issuer: "local",
subject: "user-1",
method: "local",
roles: ["user"],
permissions: ["session.use"],
roles,
permissions: storedPermissions,
userAuthRevision: 1,
authConfigRevision: "b".repeat(64),
remembered: false,
@@ -249,7 +269,13 @@ test("the session boundary touches a valid cookie session through the bounded Ta
app.get("/private", async (request) => getPrincipal(request));
const token = "z".repeat(43);
expect((await app.inject({ method: "GET", url: "/private", headers: { cookie: `thothii_session=${token}` } })).statusCode).toBe(200);
const response = await app.inject({ method: "GET", url: "/private", headers: { cookie: `thothii_session=${token}` } });
expect(response.statusCode).toBe(200);
expect(response.json()).toMatchObject({
roles,
permissions: rolesToPermissions(roles),
isAdmin: roles.includes("admin"),
});
expect(sessions.touch).toHaveBeenCalledWith(token);
});
@@ -124,8 +124,17 @@ test.skipIf(!dockerAvailable)("PostgreSQL migration enforces one database per wo
],
relationships: [],
};
expect(await repository.applySchemaSync(created.id, 1, "columns", [], fullColumnsSnapshot))
const memoryCleanupRun = await repository.createSyncRun(created.id, "columns", [], 1);
await repository.updateSyncRun(memoryCleanupRun.id, { state: "applying", phase: "applying" });
await expect(repository.applySchemaSync(created.id, 999, "columns", [], fullColumnsSnapshot, memoryCleanupRun.id))
.resolves.toBeUndefined();
expect((await repository.getSyncRun(memoryCleanupRun.id))?.phase).toBe("applying");
expect(await repository.applySchemaSync(created.id, 1, "columns", [], fullColumnsSnapshot, memoryCleanupRun.id))
.toMatchObject({ created: 4, deleted: 0 });
expect((await repository.getSyncRun(memoryCleanupRun.id))?.phase).toBe("memory_cleanup");
await repository.interruptActiveSyncRuns();
expect(await repository.getSyncRun(memoryCleanupRun.id))
.toMatchObject({ state: "interrupted", phase: "memory_cleanup" });
expect((await repository.listColumns(created.id, patients.id)).map((column) => ({
name: column.name,
sensitive: column.sensitive,
@@ -233,6 +242,7 @@ test.skipIf(!dockerAvailable)("PostgreSQL migration enforces one database per wo
});
expect(await repository.listSyncRuns(created.id)).toEqual([
expect.objectContaining({ id: syncRun.id, tableIds: [patients.id] }),
expect.objectContaining({ id: memoryCleanupRun.id, phase: "memory_cleanup" }),
]);
const lockedDatabase = await repository.create({
+42 -2
View File
@@ -117,6 +117,7 @@ async function setup(env: Record<string, string> = {}) {
});
const introspector: CatalogSchemaIntrospector = { scan };
const operations = new CatalogOperationCoordinator();
const cleanup = vi.fn(async () => ({ code: 0, stdout: JSON.stringify({ indexed: true, deleted: 0 }), stderr: "" }));
const registry = {
list: vi.fn(async () => [revision]),
listCatalog: vi.fn(async () => [{ id: "psd-clinical", name: "Policlinico San Donato", configurationState: "ready", revision }]),
@@ -124,7 +125,7 @@ async function setup(env: Record<string, string> = {}) {
readPinned: vi.fn(async () => ({ workspace, workspaceConfigPath: revision.snapshotPath })),
} as unknown as WorkspaceRegistry;
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "/missing", NODE_ENV: "test", ...env }), {
thtRunner: {} as never,
thtRunner: { withPrincipal: () => ({ runWithRuntimeSnapshot: cleanup }) } as never,
workspaceRegistry: registry,
workspaceSecretStore: new WorkspaceSecretStore({ root: secretRoot, runtimeRoot, installationId: "test" }),
catalogRepository: repository,
@@ -133,7 +134,7 @@ async function setup(env: Record<string, string> = {}) {
workspaceDiagnoser: vi.fn(),
});
return {
app, repository, database: (await repository.get(created.id))!, scan, operations,
app, repository, database: (await repository.get(created.id))!, scan, operations, cleanup,
setObserved(next: ObservedSchemaSnapshot) { observed = next; },
};
}
@@ -614,6 +615,45 @@ test("waits for confirmation and rescans before applying destructive changes", a
expect(scan).toHaveBeenCalledTimes(3);
});
test("retries committed Memory cleanup with the original removals without rescanning", async () => {
const { app, repository, database, scan, setObserved, cleanup } = await setup();
await seedCatalog(repository, database);
const observed = snapshot();
observed.tables = observed.tables.filter(t => t.name !== "visits");
observed.columns = observed.columns.filter(c => c.tableName !== "visits");
observed.relationships = [];
setObserved(observed);
cleanup.mockResolvedValueOnce({ code: 0, stdout: JSON.stringify({ indexed: false, deleted: 2 }), stderr: "" });
const started = await app.inject({ method: "POST", url: `/catalog/databases/${database.id}/sync-runs`,
payload: { version: database.version, scope: "all", tableIds: [] } });
const waiting = await waitFor(repository, started.json().id, "awaiting_confirmation");
expect(cleanup).not.toHaveBeenCalled();
await app.inject({ method: "POST", url: `/catalog/sync-runs/${waiting.id}/confirm`,
payload: { confirmationToken: waiting.confirmationToken } });
const failed = await waitFor(repository, waiting.id, "failed");
expect(failed).toMatchObject({ phase: "memory_cleanup", errorCode: "memory_cleanup_pending",
plannedDiff: { deletedTables: ["visits"] } });
expect((await repository.listTables(database.id)).map(t => t.name)).toEqual(["patients"]);
const callsBeforeRetry = scan.mock.calls.length;
const blocked = await app.inject({ method: "POST", url: `/catalog/databases/${database.id}/sync-runs`,
payload: { version: database.version, scope: "all", tableIds: [] } });
expect(blocked.statusCode).toBe(409);
// Simulate startup recovery and an unreachable DWH after schema application.
await repository.interruptActiveSyncRuns();
scan.mockRejectedValue(new Error("DWH unavailable"));
cleanup.mockResolvedValue({ code: 0, stdout: JSON.stringify({ indexed: true, deleted: 2 }), stderr: "" });
const retried = await app.inject({ method: "POST", url: `/catalog/sync-runs/${waiting.id}/retry` });
expect(retried.statusCode).toBe(202);
const completed = await waitFor(repository, waiting.id, "succeeded");
expect(completed.counts).toMatchObject({ memoryDeleted: 2 });
expect(scan.mock.calls.length).toBe(callsBeforeRetry);
expect(cleanup).toHaveBeenCalledTimes(2);
expect(cleanup.mock.calls[1]).toEqual(cleanup.mock.calls[0]);
const request = JSON.parse((cleanup.mock.calls[0] as unknown as string[])[1]!).request;
expect(request).toMatchObject({ sync_id: waiting.id, database: "warehouse",
schema_name: "datawarehouse", removed_tables: ["visits"] });
});
test("deletes every catalog table for multiple selected databases and cascades dependent metadata", async () => {
const { app, repository, database } = await setup();
const second = await repository.create({
+69
View File
@@ -0,0 +1,69 @@
import Fastify from "fastify";
import { afterEach, expect, test, vi } from "vitest";
import { evidenceRoutes } from "../src/routes/evidence.js";
import type { ThtRunner } from "../src/tht/tht-runner.js";
import type { PrincipalContext } from "../src/auth/principal.js";
const apps: ReturnType<typeof Fastify>[] = [];
afterEach(async () => { await Promise.all(apps.splice(0).map(app => app.close())); });
function setup(admin = true) {
const app = Fastify(); apps.push(app);
const principal: PrincipalContext = { issuer: "test", subject: "curator", roles: [admin ? "admin" : "user"],
permissions: admin ? ["evidence.manage"] : ["session.use"], isAdmin: admin };
app.addHook("preHandler", async request => { request.principal = principal; });
const run = vi.fn(async () => ({ code: 0, stdout: JSON.stringify({ items: [], total: 0 }), stderr: "" }));
const withPrincipal = vi.fn(() => ({ runWithRuntimeSnapshot: run }) as unknown as ThtRunner);
const consolidateEvidence = vi.fn();
const evidenceSources = vi.fn().mockResolvedValue({status: "succeeded"});
evidenceRoutes(app, { runner: { withPrincipal }, registryRoot: "/data/registry", hostRegistryRoot: "/srv/Thoth workspaces",
registry: { list: async () => [{ id: "sales", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/missing" }] },
service: { consolidateEvidence, evidenceSources } });
return { app, run, withPrincipal, principal, consolidateEvidence, evidenceSources };
}
test("browses complete local files with host paths, without a database runtime", async () => {
const { app, run, withPrincipal, principal } = setup();
const response = await app.inject("/workspaces/sales/evidence?kind=domain&concept=orders&page=2");
expect(response.statusCode).toBe(200);
expect(withPrincipal).toHaveBeenCalledWith(principal);
const [argv, snapshot] = run.mock.calls[0] as unknown as [string[], string];
expect(argv).toEqual(["evidence", "admin", "--workspace", "sales"]);
expect(JSON.parse(snapshot)).toEqual({ root: "/data/registry/repo/sales", query: { kind: "domain", concept: "orders", page: 2 } });
expect(response.json().location.workspace).toBe("/srv/Thoth workspaces/repo/sales");
expect(response.json().location.git_commands[0]).toContain("git -C '/srv/Thoth workspaces/repo'");
});
test("source actions require admin, bind the actor, and reject cross-workspace or arbitrary inputs", async () => {
const denied = setup(false);
expect((await denied.app.inject({method: "POST", url: "/workspaces/sales/evidence/sources", payload: {action: "refresh"}})).statusCode).toBe(403);
expect(denied.evidenceSources).not.toHaveBeenCalled();
const allowed = setup();
expect((await allowed.app.inject({method: "POST", url: "/workspaces/sales/evidence/sources", payload: {action: "refresh"}})).statusCode).toBe(200);
expect(allowed.evidenceSources).toHaveBeenCalledWith({workspaceId: "sales", action: "refresh", actor: "curator"});
const choice = {action: "decide", sourceId: "a".repeat(64), revision: "b".repeat(64), decision: "keep"};
expect((await allowed.app.inject({method: "POST", url: "/workspaces/sales/evidence/sources", payload: choice})).statusCode).toBe(200);
expect(allowed.evidenceSources).toHaveBeenLastCalledWith({...choice, workspaceId: "sales", actor: "curator"});
for (const payload of [{action: "refresh", url: "http://localhost"}, {...choice, actor: "forged"}, {...choice, revision: "../other"}]) {
expect((await allowed.app.inject({method: "POST", url: "/workspaces/sales/evidence/sources", payload})).statusCode).toBe(400);
}
expect((await allowed.app.inject({method: "POST", url: "/workspaces/other/evidence/sources", payload: choice})).statusCode).toBe(404);
expect(allowed.evidenceSources).toHaveBeenCalledTimes(2);
});
test.each(["/", "/evidence:rule", "/consolidate"])("refuses non-admin access %s", async suffix => {
const { app, run, consolidateEvidence } = setup(false);
const response = await app.inject({ method: suffix === "/consolidate" ? "POST" : "GET", url: `/workspaces/sales/evidence${suffix === "/" ? "" : suffix}` });
expect(response.statusCode).toBe(403); expect(run).not.toHaveBeenCalled(); expect(consolidateEvidence).not.toHaveBeenCalled();
});
test("rejects workspace escape, unknown workspace and unsupported filters", async () => {
const { app, run } = setup();
expect((await app.inject("/workspaces/foreign/evidence")).statusCode).toBe(404);
expect((await app.inject("/workspaces/sales/evidence?root=/private")).statusCode).toBe(400);
expect((await app.inject("/workspaces/sales/evidence?page_size=101")).statusCode).toBe(400);
expect(run).not.toHaveBeenCalled();
});
test("a missing unit is 404 and consolidation preserves the partial outcome", async () => {
const { app, consolidateEvidence } = setup();
expect((await app.inject("/workspaces/sales/evidence/evidence:missing")).statusCode).toBe(404);
consolidateEvidence.mockResolvedValue({ status: "failed", warnings: ["Saved; retry indexing."] });
const response = await app.inject({ method: "POST", url: "/workspaces/sales/evidence/consolidate" });
expect(response.json()).toEqual({ status: "failed", warnings: ["Saved; retry indexing."] });
});
+33 -1
View File
@@ -56,7 +56,7 @@ test("catalog listing translates upstream Pi IDs back to canonical model keys",
session: { reasoning: true, contextWindow: 32768, maxTokens: 8192 },
};
const modelCatalog: RuntimeModelCatalog = {
defaultSession: model.id, defaultMetadataGeneration: null, embedding: null,
defaultInteraction: model.id, embedding: null,
sessionModels: () => [model], metadataModels: () => [], hasSession: (id) => id === model.id,
};
try {
@@ -75,6 +75,38 @@ test("catalog listing translates upstream Pi IDs back to canonical model keys",
}
});
test("catalog listing supplies the shared DeepSeek key without requiring Pi auth", async () => {
const script = scriptWith([{ provider: "deepseek", id: "deepseek-v4-pro", name: "DeepSeek V4 Pro" }]);
const secret = join(path.dirname(script), "thothii.secrets");
writeFileSync(secret, "DEEPSEEK_API_KEY=shared-key\nOPENAI_API_KEY=unrelated-key\n", { mode: 0o600 });
const model: RuntimeModel = {
id: "deepseek/deepseek-v4-pro", provider: "deepseek", model: "deepseek-v4-pro",
label: "DeepSeek V4 Pro", upstreamModel: "deepseek-v4-pro",
authentication: { mode: "secret_env", apiKeyEnv: "DEEPSEEK_API_KEY" },
sessionAdapter: { mode: "pi_builtin" }, session: { reasoning: false },
};
const modelCatalog: RuntimeModelCatalog = {
defaultInteraction: model.id, embedding: null,
sessionModels: () => [model], metadataModels: () => [], hasSession: (id) => id === model.id,
};
try {
const lister = createPiModelLister(loadConfig({ THT_SECRETS_FILE: secret }), {
...noManagedModels, modelCatalog, loadEnabledModels: enabled(model.id),
spawnFn: (_command, _args, options) => {
expect(options.env.DEEPSEEK_API_KEY).toBe("shared-key");
expect(options.env).not.toHaveProperty("OPENAI_API_KEY");
expect(options.env).not.toHaveProperty("THT_SECRETS_FILE");
return spawn("node", [FAKE, script], { env: options.env }) as any;
},
});
await expect(lister()).resolves.toEqual([{
provider: model.provider, id: model.model, name: model.label, reasoning: false,
}]);
} finally {
rmSync(path.dirname(script), { recursive: true, force: true });
}
});
test("createPiModelLister caches within ttl (spawns once for two calls)", async () => {
const script = scriptWith([{ provider: "zai", id: "glm-5.2", name: "GLM 5.2", reasoning: true }]);
try {
+81
View File
@@ -0,0 +1,81 @@
import Fastify from "fastify";
import { afterEach, expect, test, vi } from "vitest";
import { memoryRoutes } from "../src/routes/memory.js";
import { DEFAULT_SEMANTIC_RUNTIME } from "../src/workspaces/runtime-renderer.js";
import type { ThtRunner } from "../src/tht/tht-runner.js";
import type { PrincipalContext } from "../src/auth/principal.js";
const apps: ReturnType<typeof Fastify>[] = [];
afterEach(async () => { await Promise.all(apps.splice(0).map(app => app.close())); });
const id = "mem-11111111-1111-4111-8111-111111111111";
const input = { family: "domain_clarification", subject: "Order", scope: "Sales" };
function setup(admin = true) {
const app = Fastify(); apps.push(app);
const principal: PrincipalContext = { issuer: "test", subject: "operator", roles: [admin ? "admin" : "user"],
permissions: admin ? ["memory.manage"] : ["session.use"], isAdmin: admin };
app.addHook("preHandler", async request => { request.principal = principal; });
const run = vi.fn(async () => ({ code: 0, stdout: JSON.stringify({ items: [], total: 0 }), stderr: "" }));
const bound = { runWithRuntimeSnapshot: run } as unknown as ThtRunner;
const withPrincipal = vi.fn(() => bound);
memoryRoutes(app, { runner: { withPrincipal }, runtime: DEFAULT_SEMANTIC_RUNTIME,
registry: {
list: async () => [{ id: "sales", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/missing" }],
read: vi.fn().mockResolvedValue({ workspace: { workspace: { language: "it" } } }),
} });
return { app, run, withPrincipal, principal };
}
test("list binds principal and workspace without requiring a DWH runtime", async () => {
const { app, run, withPrincipal, principal } = setup();
const response = await app.inject("/workspaces/sales/memory?q=Order&page=2&column=id");
expect(response.statusCode).toBe(200);
expect(withPrincipal).toHaveBeenCalledWith(principal);
const [args, snapshot] = run.mock.calls[0] as unknown as [string[], string];
expect(args).toEqual(["memory", "admin", "--workspace", "sales"]);
expect(JSON.parse(snapshot)).toMatchObject({ action: "list", request: { q: "Order", page: 2, column: "id" } });
expect(JSON.parse(snapshot).runtime.memoryLanguage).toBe("it");
});
test.each([
["GET", ""], ["POST", ""], ["GET", "/pending"], ["GET", `/${id}`],
["PUT", `/${id}`], ["DELETE", `/${id}`], ["POST", `/${id}/retry`],
] as const)("non-admin cannot %s %s", async (method, suffix) => {
const { app, run } = setup(false);
const response = await app.inject({ method, url: `/workspaces/sales/memory${suffix}`,
...(method === "PUT" || method === "POST" && suffix === "" ? { payload: input } : {}) });
expect(response.statusCode).toBe(403); expect(run).not.toHaveBeenCalled();
});
test("unknown workspace and invalid input do not invoke the harness", async () => {
const { app, run } = setup();
expect((await app.inject("/workspaces/missing/memory")).statusCode).toBe(404);
expect((await app.inject("/workspaces/sales/memory?page_size=101")).statusCode).toBe(400);
expect((await app.inject({ method: "POST", url: "/workspaces/sales/memory",
payload: { ...input, workspace_id: "foreign" } })).statusCode).toBe(400);
expect(run).not.toHaveBeenCalled();
});
test("create preserves saved-but-unindexed outcome, update carries complete related changes", async () => {
const { app, run } = setup();
run.mockResolvedValue({ code: 0, stdout: JSON.stringify({ id, saved: true, indexed: false }), stderr: "" });
const result = await app.inject({ method: "POST", url: "/workspaces/sales/memory", payload: input });
expect(result.statusCode).toBe(201); expect(result.json()).toEqual({ id, saved: true, indexed: false });
await app.inject({ method: "PUT", url: `/workspaces/sales/memory/${id}`, payload: {
...input, links: [{ target_id: id, meaning: "Related" }], dependencies: [{ database: "dwh" }],
} });
const [, snapshot] = run.mock.calls[1] as unknown as [string[], string];
expect(JSON.parse(snapshot)).toMatchObject({ action: "update", request: { id, card: {
links: [{ target_id: id, meaning: "Related" }], dependencies: [{ database: "dwh" }],
} } });
});
test.each([["memory_not_found", 404], ["memory_conflict", 409], ["memory_unavailable", 503]])(
"maps %s without leaking stderr", async (code, status) => {
const { app, run } = setup();
run.mockResolvedValue({ code: 1, stdout: JSON.stringify({ code, message: "sensitive detail" }), stderr: "secret" });
const result = await app.inject("/workspaces/sales/memory");
expect(result.statusCode).toBe(status); expect(result.json().code).toBe(code);
expect(result.body).not.toMatch(/secret|sensitive/);
},
);
@@ -20,9 +20,8 @@ function runtimeCatalog(overrides: Record<string, unknown> = {}, secrets = "OPEN
const catalogFile = join(root, "catalog.json");
const secretsFile = join(root, "thothii.secrets");
const catalog = {
schemaVersion: 1,
defaultSession: "zai/glm-5.3",
defaultMetadataGeneration: "zai/glm-5.3",
schemaVersion: 2,
defaultInteraction: "zai/glm-5.3",
embedding: { id: "ollama/qwen3-embedding:0.6b", dimensions: 1024 },
models: [
{
@@ -55,8 +54,8 @@ test("loads session default and safe metadata choices from the normalized runtim
const runtime = loadRuntimeModelCatalog(catalogFile);
const metadata = loadMetadataGenerationModels({ catalogFile, secretsFile });
expect(runtime.defaultSession).toBe("zai/glm-5.3");
expect(runtime.hasSession("deepseek/deepseek-v4-pro")).toBe(true);
expect(runtime.defaultInteraction).toBe("zai/glm-5.3");
expect(runtime.hasSession("deepseek/deepseek-v4-pro")).toBe(false);
expect(metadata.catalog()).toEqual({
models: [{ id: "zai/glm-5.3", label: "GLM 5.3" }],
default: "zai/glm-5.3",
@@ -69,14 +68,36 @@ test("loads session default and safe metadata choices from the normalized runtim
expect(() => metadata.resolve("zai/missing")).toThrow(MetadataGenerationModelUnavailableError);
});
test("DeepSeek uses one identity and bundle credential for native Pi and LiteLLM", () => {
const models = ["deepseek-v4-pro", "deepseek-v4-flash"].map((model) => ({
id: `deepseek/${model}`, provider: "deepseek", model, label: model, upstreamModel: model,
authentication: { mode: "secret_env", apiKeyEnv: "DEEPSEEK_API_KEY" },
sessionAdapter: { mode: "pi_builtin" }, metadataAdapter: { litellmProvider: "deepseek" },
session: { reasoning: false }, metadataGeneration: { disableThinking: false },
}));
const files = runtimeCatalog({ defaultInteraction: models[0].id, models }, "DEEPSEEK_API_KEY=shared-key\n");
const runtime = loadRuntimeModelCatalog(files.catalogFile);
const metadata = loadMetadataGenerationModels(files);
expect(runtime.sessionModels().map((model) => model.id)).toEqual(metadata.catalog().models.map((model) => model.id));
expect(metadata.catalog().default).toBe(runtime.defaultInteraction);
for (const model of models) {
expect(metadata.resolve(model.id)).toMatchObject({
id: model.id, provider: "deepseek", model: model.model,
apiKeyEnv: "DEEPSEEK_API_KEY", apiKey: "shared-key",
});
}
expect(() => metadata.resolve("deepseek-metadata/deepseek-v4-pro"))
.toThrow(MetadataGenerationModelUnavailableError);
});
test("returns empty catalogs when no runtime projection is configured", () => {
expect(loadRuntimeModelCatalog().defaultSession).toBeNull();
expect(loadRuntimeModelCatalog().defaultInteraction).toBeNull();
expect(loadMetadataGenerationModels({}).catalog()).toEqual({ models: [], default: null });
});
test("rejects a drifted default and an unprotected projection", () => {
const drifted = runtimeCatalog({ defaultSession: "zai/missing" });
expect(() => loadRuntimeModelCatalog(drifted.catalogFile)).toThrow("session default is invalid");
const drifted = runtimeCatalog({ defaultInteraction: "zai/missing" });
expect(() => loadRuntimeModelCatalog(drifted.catalogFile)).toThrow("interaction default is invalid");
const unprotected = runtimeCatalog();
chmodSync(unprotected.catalogFile, 0o666);
@@ -85,7 +106,7 @@ test("rejects a drifted default and an unprotected projection", () => {
test("rejects authentication semantics that cannot come from the installation catalog", () => {
const invalid = runtimeCatalog({
defaultMetadataGeneration: undefined,
defaultInteraction: undefined,
models: [{
id: "zai/glm-5.3",
provider: "zai",
@@ -112,3 +133,13 @@ test("splits canonical session identities without provider aliases", () => {
expect(splitCanonicalModelId("zai/glm-5.3")).toEqual({ provider: "zai", model: "glm-5.3" });
expect(() => splitCanonicalModelId("glm-5.3")).toThrow("model identity is invalid");
});
test("rejects a default supported by only one configured use", () => {
const { catalogFile } = runtimeCatalog({ defaultInteraction: "deepseek/deepseek-v4-pro" });
expect(() => loadRuntimeModelCatalog(catalogFile)).toThrow("interaction default is invalid");
});
test("requires regenerated runtime schema v2 rather than interpreting two legacy defaults", () => {
const { catalogFile } = runtimeCatalog({ schemaVersion: 1, defaultSession: "zai/glm-5.3" });
expect(() => loadRuntimeModelCatalog(catalogFile)).toThrow("runtime model catalog is invalid");
});
+100
View File
@@ -0,0 +1,100 @@
import { beforeEach, expect, test, vi } from "vitest";
import type { AppConfig } from "../src/config.js";
const fakes = vi.hoisted(() => ({
catalogRepository: { close: vi.fn(async () => {}) },
createCatalogRepository: vi.fn(),
runnerConfig: undefined as Record<string, unknown> | undefined,
acquireWorkspaceRuntime: vi.fn(),
releaseWorkspaceRuntime: vi.fn(),
run: vi.fn(async () => ({
code: 0,
stdout: JSON.stringify({ ok: true }),
stderr: "",
})),
}));
vi.mock("../src/catalog/repository.js", () => ({
createCatalogRepository: fakes.createCatalogRepository,
}));
vi.mock("../src/tht/tht-runner.js", () => ({
ThtRunner: class {
constructor(config: Record<string, unknown>) {
fakes.runnerConfig = config;
}
run = fakes.run;
acquireWorkspaceRuntime = fakes.acquireWorkspaceRuntime;
withPrincipal() {
return this;
}
},
}));
vi.mock("../src/workspaces/registry.js", () => ({
WorkspaceRegistry: class {
async listRetainedSnapshots() {
return [{ snapshotPath: "/data/workspace-registry/snapshots/revision/workspace.yaml" }];
}
},
}));
vi.mock("../src/workspaces/secret-store.js", () => ({
WorkspaceSecretStore: class {},
}));
import { runOperatorAction } from "../src/operator-command.js";
const config = {
catalogDatabase: { host: "catalog-db" },
workspaceSecretStoreRoot: "/data/workspace-secrets",
workspaceSecretRuntimeRoot: "/tmp/workspace-secrets",
workspaceRegistry: {
installationId: "test",
root: "/data/workspace-registry",
secretRoots: ["/run/secrets"],
},
thtBin: "/opt/venv/bin/tht",
harnessDir: "/app/harness",
dataRoot: "/data",
internalQdrantUrl: "http://qdrant:6333",
internalEmbeddingUrl: "http://embedding:11434",
internalEmbeddingModel: "qwen3-embedding:0.6b",
internalEmbeddingDimensions: 1024,
} as AppConfig;
beforeEach(() => {
fakes.catalogRepository.close.mockClear();
fakes.createCatalogRepository.mockReset();
fakes.createCatalogRepository.mockReturnValue(fakes.catalogRepository);
fakes.run.mockClear();
fakes.acquireWorkspaceRuntime.mockReset();
fakes.acquireWorkspaceRuntime.mockResolvedValue({
path: "/data/workspace-registry/snapshots/runtime/workspace.yaml",
release: fakes.releaseWorkspaceRuntime,
});
fakes.releaseWorkspaceRuntime.mockClear();
fakes.runnerConfig = undefined;
});
test("workflow doctor gives schema-v4 runtime rendering a live Catalog repository", async () => {
await expect(runOperatorAction("workflow-doctor", config)).resolves.toEqual({
ready: true,
workspaces: 1,
});
expect(fakes.createCatalogRepository).toHaveBeenCalledWith(config.catalogDatabase);
expect(fakes.runnerConfig?.catalogRepository).toBe(fakes.catalogRepository);
expect(fakes.acquireWorkspaceRuntime).toHaveBeenCalledWith(
"/data/workspace-registry/snapshots/revision/workspace.yaml",
);
expect(fakes.run).toHaveBeenCalledWith(
["doctor", "--json"],
"/data/workspace-registry/snapshots/runtime/workspace.yaml",
);
expect(fakes.releaseWorkspaceRuntime).toHaveBeenCalledOnce();
expect(fakes.catalogRepository.close).toHaveBeenCalledOnce();
});
+43 -4
View File
@@ -1,4 +1,4 @@
import { mkdtempSync } from "node:fs";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test, vi } from "vitest";
@@ -7,7 +7,34 @@ import {
createPiManagement,
type PiExecFile,
} from "../src/pi/management.js";
import type { RuntimeModelCatalog } from "../src/models/runtime-model-catalog.js";
import type { RuntimeModel, RuntimeModelCatalog } from "../src/models/runtime-model-catalog.js";
test.each([false, true])("catalog credential status ignores legacy Pi auth, missing bundle key: %s", async (missingKey) => {
const root = mkdtempSync(join(tmpdir(), "tht-catalog-status-"));
writeFileSync(join(root, "auth.json"), JSON.stringify({ deepseek: { type: "api_key", key: "stale-key" } }), { mode: 0o600 });
const secret = join(root, "thothii.secrets");
writeFileSync(secret, missingKey ? "THT_MODEL_API_KEY=legacy-key\n" : "DEEPSEEK_API_KEY=shared-key\n", { mode: 0o600 });
vi.stubEnv("PI_CODING_AGENT_DIR", root);
const model: RuntimeModel = {
id: "deepseek/deepseek-v4-pro", provider: "deepseek", model: "deepseek-v4-pro",
label: "DeepSeek", upstreamModel: "deepseek-v4-pro",
authentication: { mode: "secret_env", apiKeyEnv: "DEEPSEEK_API_KEY" },
sessionAdapter: { mode: "pi_builtin" }, session: { reasoning: false },
};
try {
const service = createPiManagement(loadConfig({ THT_SECRETS_FILE: secret }), {
modelCatalog: {
defaultInteraction: model.id, embedding: null,
sessionModels: () => [model], metadataModels: () => [], hasSession: (id) => id === model.id,
},
execute: successfulExec([]), readSettings: () => ({ thinking: "medium" }),
});
expect((await service.status()).credentials).toBe(missingKey ? "missing" : "present");
} finally {
vi.unstubAllEnvs();
rmSync(root, { recursive: true, force: true });
}
});
function configFor(settingsFile = join(mkdtempSync(join(tmpdir(), "tht-pi-management-")), "settings.json")) {
return loadConfig({
@@ -15,12 +42,12 @@ function configFor(settingsFile = join(mkdtempSync(join(tmpdir(), "tht-pi-manage
SETTINGS_FILE: settingsFile,
PI_BIN: "/usr/local/bin/pi",
PI_MANAGEMENT_TIMEOUT_MS: "750",
THT_HOST_PLATFORM: "linux",
});
}
const modelCatalog: RuntimeModelCatalog = {
defaultSession: "zai/glm-5.2",
defaultMetadataGeneration: null,
defaultInteraction: "zai/glm-5.2",
embedding: { id: "ollama/qwen3-embedding:0.6b", dimensions: 1024 },
sessionModels: () => [],
metadataModels: () => [],
@@ -34,6 +61,16 @@ function successfulExec(calls: Array<{ command: string; args: string[]; timeout:
};
}
test.each([
["linux", "linux"], ["darwin", "macos"], ["windows", "windows"],
])("reports installation host %s independently of the backend container OS", async (host, expected) => {
const service = createPiManagement(loadConfig({ THT_HOST_PLATFORM: host }), {
modelCatalog, execute: successfulExec([]), readSettings: () => ({ thinking: "medium" }),
credentialStatus: () => "missing",
});
expect((await service.status()).hostPlatform).toBe(expected);
});
// Catches a Pi executable that emits unexpected text or is invoked through a shell, which could
// turn a version display into a command-injection or information-disclosure surface.
test("status parses only a Pi version from a fixed execFile argument array", async () => {
@@ -47,6 +84,7 @@ test("status parses only a Pi version from a fixed execFile argument array", asy
});
await expect(service.status()).resolves.toEqual({
hostPlatform: "linux",
version: "0.80.3",
ready: true,
credentials: "missing",
@@ -78,6 +116,7 @@ test.each(["present", "missing"] as const)(
const status = await service.status();
expect(status).toEqual({
hostPlatform: "linux",
version: "0.80.3",
ready: true,
credentials,
+31 -16
View File
@@ -642,27 +642,33 @@ test("session Pi spawn reads the single secret bundle and scrubs its path", asyn
}
});
test("session Pi spawn resolves the selected catalog credential from the secret bundle", () => {
test.each([false, true])("session Pi uses the catalog bundle despite stale Pi auth: %s", (missingKey) => {
const root = mkdtempSync(path.join(tmpdir(), "thothii-catalog-credential-"));
const agentDir = path.join(root, "agent");
mkdirSync(agentDir, { mode: 0o700 });
writeFileSync(path.join(agentDir, "auth.json"), "{}\n", { mode: 0o600 });
const originalAuth = JSON.stringify({
deepseek: { type: "api_key", key: "stale-key" },
anthropic: { type: "api_key", key: "unrelated-key" },
});
writeFileSync(path.join(agentDir, "auth.json"), originalAuth, { mode: 0o600 });
writeFileSync(path.join(agentDir, "models.json"), '{"providers":{}}\n', { mode: 0o600 });
const secret = path.join(root, "thothii.secrets");
writeFileSync(secret, "ZAI_API_KEY=catalog-secret\nTHT_MODEL_API_KEY=legacy-secret\n", { mode: 0o600 });
writeFileSync(secret, missingKey ? "THT_MODEL_API_KEY=legacy-secret\n"
: "DEEPSEEK_API_KEY=catalog-secret\nTHT_MODEL_API_KEY=legacy-secret\n", { mode: 0o600 });
const legacyKey = path.join(root, "legacy-key");
writeFileSync(legacyKey, "legacy-file-key", { mode: 0o600 });
const model: RuntimeModel = {
id: "openai/test-model",
provider: "openai",
model: "test-model",
label: "Test model",
upstreamModel: "test-model",
authentication: { mode: "secret_env", apiKeyEnv: "ZAI_API_KEY" },
id: "deepseek/deepseek-v4-pro",
provider: "deepseek",
model: "deepseek-v4-pro",
label: "DeepSeek V4 Pro",
upstreamModel: "deepseek-v4-pro",
authentication: { mode: "secret_env", apiKeyEnv: "DEEPSEEK_API_KEY" },
sessionAdapter: { mode: "pi_builtin" },
session: { reasoning: false },
};
const modelCatalog: RuntimeModelCatalog = {
defaultSession: model.id,
defaultMetadataGeneration: null,
defaultInteraction: model.id,
embedding: null,
sessionModels: () => [model],
metadataModels: () => [],
@@ -672,17 +678,26 @@ test("session Pi spawn resolves the selected catalog credential from the secret
const child = recordingChild();
child.stderr.resume = () => {};
vi.stubEnv("PI_CODING_AGENT_DIR", agentDir);
const mgr = new PiProcessManager(loadConfig({ THT_SECRETS_FILE: secret }), {
const mgr = new PiProcessManager(loadConfig({ THT_SECRETS_FILE: secret, THT_MODEL_API_KEY_FILE: legacyKey }), {
modelCatalog,
authProviders: () => new Set(),
authProviders: () => new Set(["deepseek"]),
spawnFn: (...args: any[]) => { calls.push(args); return child as any; },
});
try {
mgr.createFor("catalog-credential", { provider: "openai", model: "test-model" });
expect(calls[0][2].env.ZAI_API_KEY).toBe("catalog-secret");
const create = () => mgr.createFor("catalog-credential", { provider: model.provider, model: model.model });
if (missingKey) {
expect(create).toThrow("model provider credential is unavailable");
expect(calls).toHaveLength(0);
return;
}
create();
expect(calls[0][2].env.DEEPSEEK_API_KEY).toBe("catalog-secret");
expect(calls[0][2].env).not.toHaveProperty("OPENAI_API_KEY");
expect(calls[0][2].env).not.toHaveProperty("THT_MODEL_API_KEY");
expect(JSON.parse(readFileSync(path.join(calls[0][2].env.PI_CODING_AGENT_DIR, "auth.json"), "utf8")))
.toEqual({ anthropic: { type: "api_key", key: "unrelated-key" } });
} finally {
expect(readFileSync(path.join(agentDir, "auth.json"), "utf8")).toBe(originalAuth);
mgr.teardown("catalog-credential");
vi.unstubAllEnvs();
rmSync(root, { recursive: true, force: true });
@@ -750,7 +765,7 @@ test("set_model translates a canonical catalog key to its upstream Pi model ID",
session: { reasoning: false, contextWindow: 32768, maxTokens: 8192 },
};
const modelCatalog: RuntimeModelCatalog = {
defaultSession: model.id, defaultMetadataGeneration: null, embedding: null,
defaultInteraction: model.id, embedding: null,
sessionModels: () => [model], metadataModels: () => [], hasSession: (id) => id === model.id,
};
const mgr = new PiProcessManager(loadConfig({ PI_BIN: "/usr/local/bin/pi" }), {
+7 -4
View File
@@ -61,20 +61,22 @@ test("provider smoke resolves the selected catalog credential from the secret bu
session: { reasoning: false },
};
const modelCatalog: RuntimeModelCatalog = {
defaultSession: model.id,
defaultMetadataGeneration: null,
defaultInteraction: model.id,
embedding: null,
sessionModels: () => [model],
metadataModels: () => [],
hasSession: (id) => id === model.id,
};
let spawnEnv: NodeJS.ProcessEnv | undefined;
const readAuthStore = vi.fn(() => JSON.stringify({ openai: { type: "api_key", key: "stale-key" } }));
const smoke = createPiProviderSmoke(loadConfig({ THT_SECRETS_FILE: secret }), {
modelCatalog,
authProviders: () => new Set(),
authProviders: () => new Set(["openai"]),
readAuthStore,
readModelsStore: () => undefined,
spawnFn: (_command, _args, options) => {
spawnEnv = options.env;
expect(existsSync(join(options.env.PI_CODING_AGENT_DIR!, "auth.json"))).toBe(false);
return successfulProviderChild();
},
});
@@ -85,6 +87,7 @@ test("provider smoke resolves the selected catalog credential from the secret bu
expect(spawnEnv?.ZAI_API_KEY).toBe("catalog-secret");
expect(spawnEnv).not.toHaveProperty("OPENAI_API_KEY");
expect(spawnEnv).not.toHaveProperty("THT_MODEL_API_KEY");
expect(readAuthStore).not.toHaveBeenCalled();
} finally {
rmSync(root, { recursive: true, force: true });
}
@@ -306,7 +309,7 @@ test("provider smoke makes one configured request from an isolated no-capability
sessionAdapter: { mode: "pi_builtin" }, session: { reasoning: true },
};
const smokeCatalog: RuntimeModelCatalog = {
defaultSession: smokeModel.id, defaultMetadataGeneration: null, embedding: null,
defaultInteraction: smokeModel.id, embedding: null,
sessionModels: () => [smokeModel], metadataModels: () => [],
hasSession: (id) => id === smokeModel.id,
};
+90 -12
View File
@@ -27,10 +27,9 @@ function operationalWorkspace(id = "default") {
} as const;
}
function sessionCatalog(defaultSession = "zai/glm-5.2", available = [defaultSession]) {
function sessionCatalog(defaultInteraction = "zai/glm-5.2", available = [defaultInteraction]) {
return {
defaultSession,
defaultMetadataGeneration: null,
defaultInteraction,
embedding: { id: "ollama/qwen3-embedding:0.6b", dimensions: 1024 },
sessionModels: () => [],
metadataModels: () => [],
@@ -364,6 +363,65 @@ test("retention scans a removed workspace's retained snapshot", async () => {
expect(response.json()).toEqual([expect.objectContaining({ id: "resumable" })]);
});
test("active sessions remain available when retained snapshot discovery is unreadable", async () => {
const activeSnapshot = "/registry/snapshots/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/active.yaml";
const retainedFailure = "invalid retained snapshot /registry/snapshots/secret/legacy.yaml";
const reconcileSnapshotRetention = vi.fn(async () => {});
const subscribed = vi.fn();
const warning = vi.spyOn(console, "warn").mockImplementation(() => {});
const manifest = {
id: "live-session", status: "open", archived: false,
workspace_id: "active", workspace_revision: "a".repeat(40),
};
const app = buildApp(loadConfig({ AUTH_MODE: "upstream", THT_HARNESS_DIR: "../harness" }), {
thtRunner: {
withPrincipal: () => ({
sessionList: async (snapshotPath: string) => {
expect(snapshotPath).toBe(activeSnapshot);
return [manifest];
},
sessionShow: async (id: string, snapshotPath?: string) => {
if (id === manifest.id && snapshotPath === activeSnapshot) return manifest;
throw new Error("session not found");
},
}),
} as any,
mgr: { get: () => undefined } as any,
hub: {
subscribe: (_id: string, _send: unknown, options: { close?: () => void }) => {
subscribed();
options.close?.();
return () => {};
},
} as any,
workspaceRegistry: {
listRetainedSnapshots: async () => { throw new Error(retainedFailure); },
list: async () => [{
id: "active", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: activeSnapshot,
}],
reconcileSnapshotRetention,
} as any,
});
try {
const list = await app.inject({ method: "GET", url: "/sessions", headers: aliceHeaders });
const detail = await app.inject({ method: "GET", url: `/sessions/${manifest.id}`, headers: aliceHeaders });
const events = await app.inject({ method: "GET", url: `/sessions/${manifest.id}/events`, headers: aliceHeaders });
expect(list.statusCode).toBe(200);
expect(list.json()).toEqual([expect.objectContaining({ id: manifest.id, active: false })]);
expect(detail.statusCode).toBe(200);
expect(detail.json()).toMatchObject(manifest);
expect(events.statusCode).toBe(200);
expect(subscribed).toHaveBeenCalledOnce();
expect(reconcileSnapshotRetention).not.toHaveBeenCalled();
expect(list.body + detail.body + events.body).not.toContain(retainedFailure);
} finally {
warning.mockRestore();
await app.close();
}
});
test("the single local installation listing reconciles its resumable workspace pins", async () => {
const retained = vi.fn(async () => {});
const retainedRevision = "d".repeat(40);
@@ -850,7 +908,7 @@ test("session lifecycle locates a B session when installation default is A", asy
active = undefined;
expect((await app.inject({ method: "POST", url: "/sessions/session-b/resume" })).json())
.toEqual({ id: "session-b", alreadyActive: false });
.toEqual({ id: "session-b", alreadyActive: false, workspaceId: "b-workspace" });
expect(calls).toContain(`new:${bPath}`);
expect(calls).toContain(`list:${bPath}`);
expect(calls).toContain(`show:${bPath}`);
@@ -1120,6 +1178,29 @@ test("POST /sessions/:id/resume uses the manifest's retained workspace revision"
);
});
test.each([undefined, { provider: "local", model: "qwen" }])("resume uses the global model/default, not the historical manifest (%j)", async (payload) => {
const configure = vi.fn(async () => {});
const manifest = { status: "open", archived: false, provider: "retired", model: "old-model" };
const runtime = { bridge: { onClientEvent: () => {}, emitClientEvent: () => {} } };
let current: any;
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
mgr: {
get: () => current,
createFor: () => { current = runtime; return runtime; },
configure, start: () => {},
} as any,
thtRunner: { sessionShow: async () => manifest, reopenSession: async () => {} } as any,
readiness: { ensure: async () => ({ ok: true }) } as any,
runtimeModelCatalog: sessionCatalog("zai/glm-5.2", ["zai/glm-5.2", "local/qwen"]),
getSettings: () => ({ workspace: "psd", thinking: "medium" }) as any,
});
const response = await app.inject({ method: "POST", url: "/sessions/model-resume/resume", ...(payload ? { payload } : {}) });
expect(response.statusCode).toBe(200);
await new Promise((resolve) => setImmediate(resolve));
expect(configure).toHaveBeenCalledWith(expect.anything(), expect.objectContaining(payload ?? { provider: "zai", model: "glm-5.2" }));
expect(manifest).toMatchObject({ provider: "retired", model: "old-model" });
});
test("POST /sessions/:id/resume returns a sanitized error when its retained revision is unavailable", async () => {
const rawFailure = "cannot read /data/workspace-registry/snapshots/secret-revision";
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
@@ -2632,7 +2713,7 @@ test("POST /sessions proceeds when ollamaEnsure succeeds", async () => {
expect(ensureWs).toContain(`/snapshots/${"e".repeat(40)}/psd.yaml`);
});
test("POST /sessions falls back from a stale requested model to the catalog default", async () => {
test("POST /sessions rejects a stale requested model without silently using the default", async () => {
let created = 0;
let persisted: any;
const runtime = { bridge: { onClientEvent: () => {} } };
@@ -2662,13 +2743,10 @@ test("POST /sessions falls back from a stale requested model to the catalog defa
payload: { question: "q", provider: "deepseek", model: "deepseek-v4-pro" },
});
expect(res.statusCode).toBe(200);
expect(res.json()).toEqual({
id: "fallback",
warning: "Configured model deepseek/deepseek-v4-pro is unavailable; using zai/glm-5.2.",
});
expect(persisted).toMatchObject({ provider: "zai", model: "glm-5.2" });
expect(created).toBe(1);
expect(res.statusCode).toBe(503);
expect(res.json()).toMatchObject({ code: "model_unavailable" });
expect(persisted).toBeUndefined();
expect(created).toBe(0);
});
test("POST /sessions marks a persisted session failed when runtime construction throws", async () => {
+2 -4
View File
@@ -160,8 +160,7 @@ test("GET /models returns session choices from the installation model catalog",
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
thtRunner: {} as any,
runtimeModelCatalog: {
defaultSession: "zai/glm-5.2",
defaultMetadataGeneration: null,
defaultInteraction: "zai/glm-5.2",
embedding: { id: "ollama/qwen3-embedding:0.6b", dimensions: 1024 },
sessionModels: () => [{
id: "zai/glm-5.2", provider: "zai", model: "glm-5.2", label: "GLM 5.2",
@@ -187,8 +186,7 @@ test("GET /models returns an empty list when the catalog has no session models",
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
thtRunner: {} as any,
runtimeModelCatalog: {
defaultSession: null,
defaultMetadataGeneration: null,
defaultInteraction: null,
embedding: null,
sessionModels: () => [],
metadataModels: () => [],
@@ -10,6 +10,28 @@ import {
const roots: string[] = [];
test("Evidence consolidation runs only its stage and preserves blocked Catalog readiness", async () => {
const catalog = repository();
const runChild = vi.fn(async () => ({ exitCode: 0, stdout: JSON.stringify({ status: "succeeded", counts: { documents: 2 } }), stderr: "" }));
const result = await service({ repository: catalog, runChild }).consolidateEvidence({ workspaceId: "catalog-workspace" });
expect(result.status).toBe("succeeded");
expect(runChild).toHaveBeenCalledOnce();
expect(runChild.mock.calls[0]?.[0]).toMatchObject({ argv: ["preprocess", "evidence", "--consolidate", "--json", "-c", "/dev/fd/3"] });
expect(catalog.beginPreprocessing).not.toHaveBeenCalled();
expect(catalog.finishPreprocessing).not.toHaveBeenCalled();
expect(catalog.clearPreprocessing).not.toHaveBeenCalled();
});
test("Evidence index failure reports retry without changing Catalog state", async () => {
const catalog = repository();
const runChild = vi.fn(async () => ({ exitCode: 1, stdout: JSON.stringify({ status: "failed", saved: true, error: "Saved; retry indexing." }), stderr: "private provider endpoint" }));
const result = await service({ repository: catalog, runChild }).consolidateEvidence({ workspaceId: "catalog-workspace" });
expect(result.status).toBe("failed");
expect(result.warnings).toEqual(["Saved; retry indexing."]);
expect(catalog.finishPreprocessing).not.toHaveBeenCalled();
expect(JSON.stringify(result)).not.toContain("private provider");
});
afterEach(() => {
roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }));
});
@@ -254,6 +276,24 @@ test("semantic preflight failure is persisted before returning", async () => {
);
});
test.each(["refresh", "decide"] as const)("Evidence %s calls only the source worker and preserves Catalog readiness", async action => {
const catalog = repository();
const runChild = vi.fn(async () => ({exitCode: 0, stdout: JSON.stringify({status: "succeeded", counts: {changed: 1}}), stderr: ""}));
const result = await service({repository: catalog, runChild}).evidenceSources({workspaceId: "catalog-workspace", action,
...(action === "decide" ? {sourceId: "a".repeat(64), revision: "b".repeat(64), decision: "replace" as const} : {}), actor: "Curator"});
expect(result.status).toBe("succeeded");
expect(runChild).toHaveBeenCalledWith({configPath: expect.any(String), argv: expect.arrayContaining(["evidence", "sources", action, "-c", "/dev/fd/3", "--actor", "Curator"])});
expect(catalog.beginPreprocessing).not.toHaveBeenCalled();
expect(catalog.finishPreprocessing).not.toHaveBeenCalled();
expect(catalog.clearPreprocessing).not.toHaveBeenCalled();
});
test("failed source activation reports the saved decision for retry", async () => {
const result = await service({runChild: async () => ({exitCode: 1, stdout: JSON.stringify({status: "failed", saved: true, error: "Decision saved; retry indexing"}), stderr: "secret"})})
.evidenceSources({workspaceId: "catalog-workspace", action: "decide", sourceId: "a".repeat(64), revision: "b".repeat(64), decision: "keep"});
expect(result).toMatchObject({status: "failed", warnings: ["Decision saved; retry indexing"]});
});
test("clear invalidates Catalog readiness before clearing only derived worker data", async () => {
const catalog = repository();
const runChild = vi.fn(async () => ({
@@ -1,4 +1,5 @@
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import {
existsSync,
mkdtempSync,
@@ -202,7 +203,7 @@ test("deterministic operator leases are keyed by logical identity and stable acr
workspaceSecretStore: f.workspaceSecretStore,
});
const suffix = first.inputFingerprint.slice(7, 23);
const suffix = createHash("sha256").update(first.inputFingerprint + "\n" + readFileSync(first.path, "utf8")).digest("hex").slice(0, 16);
expect(second.path).toBe(first.path);
expect(first.path).toBe(join(
f.dataRoot,
@@ -242,6 +242,7 @@ test("separate runtime leases hand off byte-identical revision Evidence configs
source_identity: "workspace://psd-clinical",
});
expect(parse(firstYaml).evidence).toEqual({
local_archive_root: join(f.registryConfig.root, "repo", "psd-clinical"),
sources: [{
type: "filesystem",
root: expectedRoot,
@@ -192,6 +192,7 @@ test("renders filesystem Evidence below the immutable revision content root with
expect(rendered.runtime_identity.workspace_revision).toBe(evidenceRevision);
expect(rendered.evidence).toEqual({
schema_version: 2,
local_archive_root: "/srv/registry/repo/psd-clinical",
sources: [{
type: "filesystem",
root: `/srv/registry/snapshots/${evidenceRevision}/psd-clinical/evidence`,
@@ -203,7 +204,7 @@ test("renders filesystem Evidence below the immutable revision content root with
max_chunk_chars: 4_000,
retain_published_generations: 3,
});
expect(yaml).not.toContain("/srv/registry/repo");
expect(rendered.evidence.sources[0].root).not.toContain("/srv/registry/repo");
});
test("renders public HTTP Evidence with exact fractional-second timeouts and every policy limit", () => {
@@ -223,6 +224,7 @@ test("renders public HTTP Evidence with exact fractional-second timeouts and eve
}));
expect(rendered.evidence).toEqual({
local_archive_root: "/srv/registry/repo/psd-clinical",
sources: [{
type: "http",
urls: ["https://evidence.example.test/guide.md"],
+3 -2
View File
@@ -13,6 +13,7 @@ services:
THT_BIN: /opt/venv/bin/tht
THT_DATA_ROOT: /data
SETTINGS_FILE: /data/settings/settings.json
THT_EVIDENCE_HOST_REGISTRY_ROOT: ${THT_WORKSPACE_REGISTRY_ROOT:-}
THT_MAINTENANCE_FILE: /data/settings/maintenance.json
THT_WORKSPACE_REGISTRY_ROOT: /data/workspace-registry
THT_WORKSPACE_GIT_REMOTE: ${THT_WORKSPACE_GIT_REMOTE:?set THT_WORKSPACE_GIT_REMOTE}
@@ -99,7 +100,7 @@ services:
image: thothii-core:local
profiles: [catalog-maintenance]
pull_policy: never
command: ["node", "/app/backend/dist/catalog/migrate.js"]
command: ["bash", "/app/docker/catalog-migrate.sh"]
environment:
THT_CATALOG_DB_HOST: catalog-db
THT_CATALOG_DB_PORT: "5432"
@@ -153,7 +154,7 @@ services:
- type: volume
source: workspace-registry
target: /data/workspace-registry
read_only: true
read_only: false
- type: volume
source: sessions
target: /data/sessions
+20
View File
@@ -0,0 +1,20 @@
# Optional local profile: expose the existing curator checkout on the installation host.
# Copy the existing registry repo to this path before enabling the override. Snapshot/state
# volumes remain unchanged. THT_EVIDENCE_HOST_REGISTRY_ROOT must be an absolute host path.
services:
core:
environment:
THT_EVIDENCE_HOST_REGISTRY_ROOT: ${THT_EVIDENCE_HOST_REGISTRY_ROOT:?set an absolute host registry path}
volumes:
- type: bind
source: ${THT_EVIDENCE_HOST_REGISTRY_ROOT:?set an absolute host registry path}/repo
target: /data/workspace-registry/repo
bind:
create_host_path: false
workspace-maintenance:
volumes:
- type: bind
source: ${THT_EVIDENCE_HOST_REGISTRY_ROOT:?set an absolute host registry path}/repo
target: /data/workspace-registry/repo
bind:
create_host_path: false
+1 -1
View File
@@ -45,7 +45,7 @@ services:
- type: bind
source: ${THT_WORKSPACE_REGISTRY_ROOT:?set THT_WORKSPACE_REGISTRY_ROOT}
target: /data/workspace-registry
read_only: true
read_only: false
- type: bind
source: ${THT_DATA_ROOT:?set THT_DATA_ROOT}/workspace-secrets
target: /data/workspace-secrets
@@ -0,0 +1,4 @@
# The image is tagged from the running frontend before the visual review is published.
services:
frontend:
image: thothii-frontend:before-visual-review-20260912
+8
View File
@@ -0,0 +1,8 @@
# Local-only review override. Apply after the existing local installation profiles.
# Changes only the frontend image; Core, configuration and volumes remain unchanged.
services:
frontend:
image: thothii-frontend:visual-review-20260912
build:
context: /Users/mp/projects/ThothII-visual-review
dockerfile: docker/frontend.Dockerfile
+17
View File
@@ -0,0 +1,17 @@
services:
core:
environment:
THT_EVIDENCE_HOST_REGISTRY_ROOT: /Users/mp/projects/ThothII/deploy/psd/evidence-registry
volumes:
- type: bind
source: /Users/mp/projects/ThothII/deploy/psd/evidence-registry/repo
target: /data/workspace-registry/repo
bind:
create_host_path: false
workspace-maintenance:
volumes:
- type: bind
source: /Users/mp/projects/ThothII/deploy/psd/evidence-registry/repo
target: /data/workspace-registry/repo
bind:
create_host_path: false
Submodule deploy/psd/evidence-registry/repo added at 24fb53236b
+9 -3
View File
@@ -10,22 +10,28 @@ workspaceRepository:
access: ssh
modelCatalog:
defaults:
session: zai/glm-5.3
metadataGeneration: zai/glm-5.3
interaction: zai/glm-5.3
embedding:
id: ollama/qwen3-embedding:0.6b
dimensions: 1024
providers:
deepseek:
authentication:
mode: pi_auth
mode: secret_env
apiKeyEnv: DEEPSEEK_API_KEY
session:
mode: pi_builtin
metadataGeneration:
litellmProvider: deepseek
models:
deepseek-v4-pro:
label: DeepSeek V4 Pro
session: {}
metadataGeneration: {}
deepseek-v4-flash:
label: DeepSeek V4 Flash
session: {}
metadataGeneration: {}
zai:
endpoint:
baseUrl: https://api.z.ai/api/coding/paas/v4
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
node /app/backend/dist/catalog/migrate.js
/opt/venv/bin/python -m tht.memory.migrate
+1 -1
View File
@@ -131,7 +131,7 @@ ENV PATH="/opt/venv/bin:/usr/local/bin:$PATH" \
HOME=/home/thoth
COPY scripts/verify-line-endings.sh /usr/local/bin/verify-line-endings
COPY docker/core-entrypoint.sh docker/workspace-maintenance-entrypoint.sh docker/session-migrate.sh docker/ensure-pi-trust.mjs docker/embedding-model-init.sh /app/docker/
COPY docker/core-entrypoint.sh docker/workspace-maintenance-entrypoint.sh docker/session-migrate.sh docker/catalog-migrate.sh docker/ensure-pi-trust.mjs docker/embedding-model-init.sh /app/docker/
COPY docker/smoke/core-smoke.sh /app/docker/smoke/core-smoke.sh
RUN /usr/local/bin/verify-line-endings /app/docker \
&& chmod +x /app/docker/core-entrypoint.sh /app/docker/workspace-maintenance-entrypoint.sh /app/docker/session-migrate.sh /app/docker/embedding-model-init.sh /app/docker/smoke/core-smoke.sh
+14 -6
View File
@@ -5,12 +5,20 @@ COPY tools/tht/go.mod tools/tht/go.sum ./
RUN go mod download
COPY tools/tht ./
RUN mkdir -p /out \
&& CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o /out/tht-windows-amd64.exe ./cmd/tht \
&& CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o /out/tht-darwin-amd64 ./cmd/tht \
&& CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -trimpath -ldflags='-s -w' -o /out/tht-darwin-arm64 ./cmd/tht \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o /out/tht-linux-amd64 ./cmd/tht \
&& CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -ldflags='-s -w' -o /out/tht-linux-arm64 ./cmd/tht
ARG THT_VERSION=0.0.0-dev
ARG THT_COMMIT=unknown
ARG THT_BUILD_TIME=unknown
RUN linker_flags="-s -w \
-X github.com/aritmolab/thothii/tools/tht/internal/version.semanticVersion=${THT_VERSION} \
-X github.com/aritmolab/thothii/tools/tht/internal/version.commit=${THT_COMMIT} \
-X github.com/aritmolab/thothii/tools/tht/internal/version.buildTime=${THT_BUILD_TIME}" \
&& mkdir -p /out \
&& CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags="$linker_flags" -o /out/tht-windows-amd64.exe ./cmd/tht \
&& CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -trimpath -ldflags="$linker_flags" -o /out/tht-darwin-amd64 ./cmd/tht \
&& CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -trimpath -ldflags="$linker_flags" -o /out/tht-darwin-arm64 ./cmd/tht \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="$linker_flags" -o /out/tht-linux-amd64 ./cmd/tht \
&& CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -ldflags="$linker_flags" -o /out/tht-linux-arm64 ./cmd/tht
FROM scratch AS export
COPY --from=build /out/ /
@@ -0,0 +1,59 @@
---
status: accepted
date: 2026-09-08
---
# Use PostgreSQL for Memory and Qdrant for retrieval
The new Memory module uses the installation's existing PostgreSQL service as the
authority for Memory Cards, their links, and structured schema dependencies.
Qdrant holds a rebuildable search projection. This replaces the JSONL registry and
allows related administrative changes to be coordinated in one database transaction.
The owner accepted this direction in Q9 of the Memory design interview; implementation
is pending.
Memory uses its own tables and remains a separate module from the Metadata Catalog.
Sharing the PostgreSQL service does not transfer Memory ownership to Database
management or make Evidence and Memory one canonical domain.
## Retrieval and graph
The owner also accepted hybrid semantic/lexical Qdrant retrieval with scope filters
and explicit card links traversed in core. Both belong to the planned first version;
there is no dedicated graph database or external Memory framework.
This extends [ADR 0017](0017-separate-reference-vectors-from-runtime-memory.md):
the separate reference and memory collections remain, while Memory gains sparse
lexical indexing in addition to dense vectors. Memory projections become rebuildable
from the module's PostgreSQL authority. Preprocessing Clear still preserves Memory;
this decision does not add it to the preprocessing cleanup scope.
Links support discovery. Finding a card through a link does not approve its use.
The core proposes links with the cards for the same final review; Administration
provides manual creation, editing, and deletion. Deleting a card removes its incident
links without deleting the other linked cards.
## Considered options
- Retaining JSONL preserves the current storage format, but leaves coordinated
card/link/dependency mutations and concurrent administrative writes to application code.
- Using Qdrant as the sole authority is a viable alternative for record storage and
retrieval. PostgreSQL is preferred for the coordinated mutations of the new module,
with Qdrant reserved for its search projection.
- Adding a dedicated graph database would introduce another service; the selected
bounded traversal can be implemented in core over persisted links.
## Consequences
The module needs a persistence contract, PostgreSQL schema, and explicit propagation
of additions, updates, and deletions to Qdrant. Choosing PostgreSQL does not make this
propagation atomic across both systems: failures, retries, and invalidation of stale
search content must be handled and tested. An index rebuild uses the original card
content and cannot resurrect deleted cards.
The decision does not introduce Memory revision history or require compatibility with
existing development sessions. Evidence authoring and publication remain governed by
their own contract until the Evidence management project defines its evolution.
The [Memory management project](../plans/2026-09-08-memory-management.md) records the
approved behavior, scope, and integration work.
@@ -0,0 +1,152 @@
---
status: accepted
date: 2026-09-08
---
# Maintain local Evidence with external editors and manual consolidation
Evidence management must provide complete access to registered Evidence and a
maintenance path, as accepted in Q11–Q15 and revised during simplification. The owner subsequently
clarified that a context specialist writes drafts independently of the installation
and without PostgreSQL access; the system refines and stores them locally for use
and maintenance. Implementation is pending.
The storage mechanics of Q11 were revisited in the
[simplification review](../plans/2026-09-08-memory-evidence-simplification-review.md).
The original choice combined the workspace repository as Evidence authority with
application-managed Git writes. The owner accepted the separation of external draft
files/repositories from a durable local canonical file archive and removes
commit/push from normal CRUD. The management surface must expose discoverable,
editable Markdown for domain specialists, without JSONL editing. The owner chose
external editors for release 0: their preferred Mac/PC editor or vim/nano on the
server. No web editor or content form is required. The page keeps browsing,
filtering, and detail, with actual host file paths and maintenance instructions.
The owner also requested manual consolidation followed by manual Git diff, commit,
and push, relying on operator discipline rather than automation. The owner accepted
the remaining simplifications and requested explicit clarification of the core
format change and the simple terminal-based Git check. The original application Git writer
is not the final implementation prescription. A suggestion
to move Evidence authority into PostgreSQL was withdrawn after the clarification.
Memory remains a distinct module with its own PostgreSQL authority under
[ADR 0018](0018-use-postgres-for-memory-and-qdrant-for-retrieval.md).
## Manual consolidation and Git follow-up
The external-editor decision replaces the form's single Save with saving files and
running one consolidation command. The command checks expected structure, required
fields, types, identifiers, references, and provenance. It reports the affected file
and the necessary correction. Invalid input stops activation; valid input updates
derived metadata, the corpus, and the Evidence index. New and deleted files use the
same path. An inaccessible archive must not be interpreted as deleted Evidence.
Structural validation does not prove semantic correctness or rerun model refinement
to rewrite manually edited rules.
The core reads only successfully consolidated active content, never the editable
working files directly. Candidate validation and activation must preserve the last
valid corpus on failure, or report unavailability if its integrity cannot be
guaranteed. Unconsolidated edits must not mix new text with old retrieval data.
Consolidation reports full local success only once the updated content is available
to the core. If persistence succeeds but activation fails, the command reports the
partial outcome and can be rerun without duplicating units or losing edits.
The operation does not imply atomicity across
authoritative storage and Qdrant; their failure and recovery behavior requires
explicit implementation. An interrupted operation must not leave a partial corpus
advertised as ready for subsequent use.
The maintained archive is a persistent Git working tree containing the local
Evidence; it may reuse the workspace repository. Draft sources and curated units
remain distinct even when in the same repository. Setup and the page identify the
repository, working tree, actual host paths, branch, and configured remote. A
rebuildable runtime snapshot is not the editing location.
Before consolidation, the operator checks Git status, including inspection of new
untracked files. Normal terminal Git diff commands are available for line-level
inspection when needed; no custom viewer or mandatory double review is required.
After consolidation, the operator stages the Evidence and required metadata changes,
commits, and pushes using normal Git commands. No watcher, automatic Git writes, retrying push, pull, merge,
or dedicated web execution control is required. Missing credentials, unconfigured
upstreams, and Git conflicts are handled by the operator.
Consolidation activates local changes before commit/push. If the operator omits the
Git steps or push fails, the local update remains effective while transfer to the
remote is incomplete. Pushing does not automatically update other installations.
The system relies on operator discipline to complete the sequence; it does not add
a separate editorial publication state machine or require a second reviewer.
Approved conflict repairs from the core still call the same persistence and
activation service directly. They do not require an external editor or manual
consolidation before the session can use its own approved correction. Their local
file changes are included in the operator's subsequent Git maintenance.
## Administration without concurrent core work
The owner's simplification instruction replaces the earlier Q12 requirement to
refresh open sessions after administrative edits. Core activity can be assumed
absent during administration, or its overlap can be ignored. No dedicated live
update, session notification, restart, maintenance mode, or reader coordination
is required. Completed administrative changes apply to subsequent work.
Deliberate writes from the workflow itself still exist: approved conflict repairs
and the final Memory summary use the same persistence services and handle their
outcome before proceeding. In particular, a session must be able to use its own
approved Evidence correction. This does not require updating all other sessions
or rewriting previously approved decisions, artifacts, or SQL.
## Manual corrections survive source updates
When an updated source contradicts an administrator's correction, the saved manual
Evidence remains active. Evidence management shows the conflicting content for an
explicit decision and subsequent save. The source's newer content does not
automatically override the correction. The owner accepts that the manual rule can
remain in use until that comparison is resolved.
Deleted Evidence must not silently reappear after preparation or reindexing. The
authoring implementation must preserve both manual corrections and suppression of
deleted units through source refreshes. The current protection for uncommitted Git
changes is insufficient: it does not preserve already committed manual corrections
against regeneration, and retirement currently removes the unit without recording
suppression for later preparation.
## Manual creation and explicit source refresh
Administrators can create Evidence without providing an external document. In R0,
they add Markdown using the documented example and consolidate it. The application
records a manual declaration as the managed source of the current statement.
Explicit consolidation approves that declaration; it does not
claim independent documentary verification.
A correction that changes a unit's meaning uses the same explicit manual origin.
The original document remains linked for provenance and source-change detection,
but its excerpt is not presented as support for a rule it does not contain. The
canonical contract must distinguish the current supporting source from the original
document rather than silently retaining outdated support metadata.
External sources are reacquired only when an administrator requests a source
refresh. Ordinary saves and session lookups use the acquired local content; they
do not poll sources or fetch their current versions. A remote change is therefore
detected at the next requested refresh, when the manual-precedence rule applies.
The revised storage recommendation reads the specialist's repository as an input;
ordinary local edits do not write back to it or refresh its content automatically.
## Implementation consequences
The existing [Evidence lifecycle](../evidence.md) and
[Workspace Evidence v3 contract](../contracts/workspace-evidence-v3.md) require
explicit evolution for editable local Markdown, manual consolidation, and manual
precedence. Source provenance and coherent canonical metadata remain requirements;
manual changes must not be presented as statements supported by an unrelated source
excerpt. Under the accepted local-file direction, curated files are primary
installation data preserved by Clear and backed up separately from rebuildable
indexes. Visible Markdown content must become authoritative for editing; operators
must not maintain hidden duplicate text, hashes, or manifest entries themselves.
This is a core Evidence contract change: parser, renderer, authoring, validation,
normalization, and their integration with indexing and recall must be adapted
together. E1 includes a new Curated unit format version, conversion of existing
Evidence, reindexing, and end-to-end verification that visible edits reach core
consumption. Preserve the internal typed model where possible; the change does
not require redesigning the NL-to-SQL workflow phases.
Local edits do not automatically change external drafts or another installation;
Git versioning and transfer occur through the explicit manual follow-up.
The [Evidence management project](../plans/2026-09-08-evidence-management.md)
records the implementation sequence and acceptance checks.
@@ -0,0 +1,27 @@
# Unify administration pages and use namespaced routes
status: accepted
Workspace and Pi management will become full Administration Pages alongside Evidence, Memory and
Database, using one shared Administrative Page Family. The Embedded Thoth Shell will identify the
active Administration Surface through a namespaced browser route, `thoth_route=administration/<surface>`,
so links remain reloadable and support Back/Forward without requiring new host-portal server routes.
Transient form values and secrets stay outside the route; destructive confirmations may remain
short-lived dialogs because they are actions, not page containers.
We considered React-only state, path-based routes and hash routes. React-only state loses refresh and
deep-link behavior, path routes require a host catch-all route that does not yet exist in Omics Portal,
and hash routes can conflict with host-page fragments. A namespaced query route preserves the current
same-document integration boundary while leaving room for a future path adapter.
The accepted visual direction is A / Workbench for all five surfaces; B and C remain recoverable
prototypes. Workspace owns readiness and preprocessing, consuming the Database catalog as a
prerequisite. Database owns connection/binding, schema synchronization, descriptions and sensitivity;
the preprocessing action remains exclusively in Workspace preparation.
On 2026-09-12, the owner refined this direction in
[Gitea #29](https://git.tylconsulting.it/mptyl/ThothII/issues/29): Database opens at its
catalog list, with its original table space and margins, and no Workspace preparation footer.
This supersedes the earlier requirement for a preparation link in that page. Workspace
remains reachable through the shared Administration navigation; its Preparation tab still
links to Database configuration and schema when catalog work is needed.
+6 -1
View File
@@ -33,11 +33,16 @@ The production role expansion from `backend/src/auth/config.ts` is exact:
| Role | Permissions |
|---|---|
| `user` | `session.use` |
| `admin` | `session.use`, `session.read_all`, `session.manage_all`, `settings.manage`, `workspace.manage`, `workspace.secrets.manage`, `database.manage`, `pi.manage`, `auth.diagnostics.read` |
| `admin` | `session.use`, `session.read_all`, `session.manage_all`, `settings.manage`, `workspace.manage`, `workspace.secrets.manage`, `database.manage`, `memory.manage`, `evidence.manage`, `pi.manage`, `auth.diagnostics.read` |
`admin` therefore includes the ordinary `session.use` permission. No other role or permission
label is part of the production catalog.
After validating a browser session, the backend expands its roles through the current permission
catalog on every request. The permissions saved at login are a historical snapshot, so existing
administrator sessions can use newly deployed administration features without signing in again.
Session expiry, revocation and local-user role validation still apply before role expansion.
OIDC is provider-neutral at the browser protocol boundary. Authorization Code + PKCE, issuer,
signature, audience, expiry, state, and nonce are validated before a principal is created.
Authentik is the first certified group-catalog adapter, not a special browser login mode.
+4 -1
View File
@@ -7,7 +7,9 @@ This page complements the [architecture overview](overview.md) with the module s
The frontend communicates with the backend through REST and SSE. The backend does not own session
persistence: it starts Pi, invokes the `tht` CLI, and forwards events. It does own the separate
installation-local database catalog. The harness contains the workflow, the Python CLI, and
adapters for the DWH and vector store.
adapters for the DWH and vector store. Its Memory module also owns the authoritative
PostgreSQL archive of cards, links, dependencies and pending Qdrant projections.
Administrative API calls use the same harness service as workflow producers and recall.
```mermaid
flowchart LR
@@ -20,6 +22,7 @@ flowchart LR
THT --> FS["Sessions and artifacts\nworkspace repository"]
THT --> DWH["DWH\nread-only"]
THT --> VDB["Qdrant / vector store"]
THT --> MEM["thoth_memory\nPostgreSQL Memory archive"]
BE --> CFG["settings.json\nworkspace + thinking"]
BE --> MODELS["generated runtime catalog\nfrom installation YAML"]
BE --> CAT["catalog-db\nPostgreSQL + Kysely"]
+65
View File
@@ -0,0 +1,65 @@
# Session corrections to Memory and Evidence
`reviewer_archive_repair` presents one to five closed alternatives for a conflict.
Each alternative updates one existing Memory Card or one existing local Evidence unit,
showing complete current and resulting content. The reviewer chooses one alternative,
rejects all as inadequate, or asks for reformulation. A correction does not approve or
advance the session phase; the ordinary gate still reviews its use in the current question.
The harness coordinator `tht/archive_repair.py` joins two independent domains. It uses
Memory's PostgreSQL repository, workspace mutation lock and projection service, and the
same local Evidence save/consolidation boundary as administration. Evidence activation
runs the existing corpus pipeline without reacquiring configured external sources.
No database binding, schema configuration, other archive, or Git repository is changed.
## Authority and recovery
Migration `004_archive_repairs.sql` stores session-bound proposals and receipts in
`thoth_memory.archive_repairs`, isolated by workspace RLS. Each receipt captures the
session decision context, current target revision, complete resulting content, selected
choice, acting principal and publication outcome. The preparation command changes no
archive content. Application accepts only an option ID from the persisted proposal.
Memory writes its new revision and receipt in one SQL transaction. Projection failures
leave a durable pending operation; retry propagates the saved revision. A later card
edit invalidates replay of the correction.
Evidence records the exact choice before writing its canonical file. Recovery accepts
either the reviewed original revision or the already-written approved result; it never
overwrites a different intervening correction. Before a first proposal, the editable
checkout must match its active snapshot. Other curated files are fingerprinted and
checked again before application/retry so a session decision cannot publish unrelated
external edits. A crash after replacement is recoverable by the same receipt. Original
document provenance is retained as the lineage of the manual correction.
The gate displays these outcomes:
| Status | Meaning |
| --- | --- |
| `proposed` | Waiting for a human choice; no content saved |
| `rejected` | All alternatives declined; reformulation required |
| `applying` | Choice recorded; file write or candidate recovery still required |
| `pending_activation` | Content saved; index activation incomplete |
| `active` | Saved correction matches the currently active revision |
| `superseded` | The target was removed or changed after the saved correction |
The reviewer may retry a pending correction or continue the current question while
leaving activation explicitly pending. The latter is not persistent-resolution success.
`repair-show` refreshes target status; `repairs` lists the historical receipt status.
The final Memory summary must not create a duplicate of a correction already saved here.
## Authorization and commands
Inspection/preparation requires an accessible open session in the same workspace.
Applying either archive correction requires an administrator. Browser responses are
also checked against the responding principal's `memory.manage` or `evidence.manage`
permission and the Pi runtime owner. An administrator using another principal's runtime
must resume it under their own account first, preserving truthful receipt attribution.
Non-administrators may reject proposals or continue question review without changing
the archives. The Pi shell guard blocks `repair-apply`; only the human gate invokes it.
The Python workflow CLI provides `memory repair-target`, `repair-prepare`, `repair-show`,
`repair-apply`, and `repairs`. These are workflow integration commands, not new native
installation commands. All accept `--session` and command-local `-c`; JSON output remains
machine-readable. Proposal bodies are bounded to 1 MB. Durable receipts support both
filesystem and server session storage without a persisted chat transcript.
+248
View File
@@ -0,0 +1,248 @@
# Editable Curated Evidence v4
Curated unit v4 makes the visible Markdown body authoritative. It uses the existing
typed Evidence payloads and stable identifiers. Workspace descriptor v4 and Evidence
descriptor v1/v2 are separate version numbers.
E1 implements the format, explicit conversion, local archive and consolidation API.
E2 connects that API to the installed consolidation command, Evidence management
page and runtime source selection. The local PSD preview now uses 35 converted units.
## Write or edit a file
Place units in `<workspace-root>/evidence/curated/<kind>/<name>.md`. Keep the existing
`id` when editing. The directory must match `kind`. A new manual unit needs no external
document, hash or encoded metadata:
```markdown
---
schema_version: 4
id: evidence:order-key
kind: domain
language: en
purposes: [sql_generation]
applies_to:
tables: [sales.orders]
---
# Order key
## Rule
Join orders using the order number, financial year and company.
```
Required metadata is `schema_version`, `id`, `kind`, `language`, and a nonempty
`purposes` list. Purposes are `disambiguation`, `rewriting`, `schema_linking`, and
`sql_generation`. Optional `applies_to` contains `concepts`, `tables`, and `columns`.
Tables use `schema.table`; columns use `schema.table.column`.
The first H1 is the title. H2 headings identify the payload fields below. Heading
spelling follows `language`: Italian for `it` and its regional variants, English
otherwise. Keep structural headings when editing the text below them.
| Kind | English field headings | Italian field headings |
| --- | --- | --- |
| `domain` | Rule | Regola |
| `glossary` | Definition, Synonyms, Variants | Definizione, Sinonimi, Varianti |
| `enum` | Column, Values | Colonna, Valori |
| `example` | Question, Interpretation | Domanda, Interpretazione |
| `mapping` | Concept, Tables, Columns | Concetto, Tabelle, Colonne |
| `normalization` | Input, Output, Rule | Input, Output, Regola |
| `formula` | Concept, Columns, SQL | Concetto, Colonne, SQL |
| `reference` | URL, Label, Description | URL, Etichetta, Descrizione |
List fields use one `- value` per line. Empty optional lists may be omitted. Quoted
JSON strings within bullets preserve unusual or multiline values during conversion.
Enum values use `### "stored value"`, followed by their meaning; `### ""` represents
an empty stored value. Formula SQL uses a fenced `sql` block containing one PostgreSQL
expression. Whole queries and mutation statements remain invalid.
Nested prose headings and fenced examples are supported inside text fields. An H2
matching a field heading is structural outside a code fence. Duplicate fields, missing
required fields, duplicate metadata keys and malformed payloads are rejected. There
is no second title or payload in frontmatter and no hidden authoritative rule text.
Conversion fails explicitly if a legacy payload cannot be represented losslessly.
## Current provenance and original source
The host supplies document provenance when refining source material: relative source
path, normalized source hash and exact supporting excerpts. A manual unit can omit
`provenance`. Consolidation records `kind: manual` and the supplied curator identity.
A visible change to a previously recorded unit becomes a manual declaration. If that
unit originated from a document, its former document provenance is retained under
`original`. The original excerpts establish lineage; they do not assert that the
source contains the new wording. Retrieval carries this distinction through typed
metadata. Curators edit content; the service updates managed provenance.
Unchanged document declarations still require matching source bytes and excerpts.
Replacing a source requires an explicit refresh through the E3 source-review path. Ordinary preparation
is blocked on an initialized local archive, preventing regenerated source material
from overwriting corrections or restoring deletions. Legacy `resolve` is likewise
blocked there; local file corrections and the archive API own those changes.
## Persistent archive and activation
```text
<workspace-root>/
.evidence-archive.lock
evidence/
source/ # acquired original documents
curated/<kind>/*.md # editable primary content
local-manifest.yaml # derived declarations and deletion records
.local/
state.yaml # baseline, pending and active revisions
snapshots/<revision>/ # immutable units, source bytes and manifest
```
Keep the complete Evidence tree and its managed metadata in backups and the operator's
Git review. Historical source bytes and deletion records are needed to preserve manual
care across subsequent imports. The separate corpus cache and Qdrant index are derived.
The lock file only coordinates local service operations.
`LocalEvidenceArchive.initialize()` records the pre-edit baseline without activation.
`consolidate(actor=..., activate=...)` validates files, derives provenance, records
deletions and source suppression, and creates an immutable candidate. The activation
callback receives that snapshot and must raise if indexing is blocked or fails. Only
successful activation advances `active_snapshot()`. Without a callback the result is
explicitly `pending_activation`; saving a file alone never changes this pointer.
The same candidate can be retried after an index failure. Interrupted managed writes
are replayed only when the operator's file bytes have not changed. A missing curated
directory is an availability error, not proof that all units were deleted. Removing
unit files from an accessible directory records deletion without deleting their sources.
The API also provides `get`, revision-checked `save`, and revision-checked `remove` for
future deliberate workflow corrections. Concurrent external edits produce conflicts.
These boundaries are exercised with the existing corpus pipeline and real Qdrant.
An initialized installation reads only its active local snapshot, including during
ordinary preprocessing. Unconsolidated edits are visible in Administration but do
not enter retrieval. Before initialization, the pinned repository source still works.
## Administration and installed command
Open **Administration → Evidence management**. This independent page requires the
`evidence.manage` permission and no active session. It shows complete units, source
lineage, review items, file errors, filters, and changes relative to the active snapshot.
Edit the displayed Markdown path using an external editor. Refresh files to inspect
the result, then run the command shown by the page:
```sh
tht --installation /absolute/path/thothii-installation.yaml \
workspace evidence consolidate --workspace psd-clinical
```
The installed command accepts `--json`. It validates and activates Evidence using
the existing corpus pipeline and embedding service. It does not scan the DWH, run
full workspace preprocessing, change Catalog/Schema readiness, or execute Git.
If indexing fails after saving, the archive retains the candidate for retry and the
previous active revision remains selected. Validation errors identify corrections
to make in the files. Structural and review checks still apply; initialized archives
do not require the legacy repository's fixed retrieval-evaluation fixture, whose
expected IDs would otherwise prevent deliberate local deletions.
The canonical workspace root is `<workspace-registry-root>/repo/<workspace-id>`.
Snapshots and the derived corpus are separate. For a registry in a Docker volume,
copy its existing `repo` to a persistent host directory before enabling
`deploy/compose.evidence-host.yaml`; set `THT_EVIDENCE_HOST_REGISTRY_ROOT` to that
directory and include the override in the installation descriptor. Core and
workspace-maintenance must mount the same checkout. Keep registry state/snapshots
on their existing volume. The page only presents a host path when configured; it
does not label an internal container path as a usable editor path.
After successful consolidation, inspect and commit the complete workspace Evidence
tree, including managed manifests, snapshots, and deletion records, then push manually.
The page provides quoted POSIX-shell examples for status, diff, add, commit, and push.
Do not commit only the edited Markdown. Ordinary Git operations remain the operator's
responsibility. E3 source decisions use this same activation boundary.
**Clear** removes derived Reference/corpus data while retaining editable files,
snapshots and Memory. It still requires full workspace preprocessing to recreate
Reference/Schema readiness; Evidence consolidation does not satisfy that gate.
## Import drafts and refresh sources
Place externally authored Markdown drafts in `<workspace-root>/evidence/incoming/`.
The specialist needs no installation account or database access to write a draft.
Copying the draft to the installation and choosing **Import or refresh sources** in
Evidence management explicitly starts acquisition and refinement. The equivalent
installed command is:
```sh
tht --installation /absolute/path/thothii-installation.yaml \
workspace evidence refresh --workspace psd-clinical
```
This reads local `incoming/**/*.md` and original `source/**/*.md` files, excluding
managed `source/acquired/` versions. It also reads configured HTTP and S3 sources
through the existing read-only adapters and network policies. After local archive
initialization, filesystem descriptors use this local authoring tree; ordinary
runtime/preprocessing never fetches remote source changes. No source-server write
credential is needed. The existing Pi authoring refiner runs once for each changed
document, without session state or tools. Unchanged source hashes skip refinement.
All acquisitions and proposals must succeed before the new comparison set is
recorded. An access or refinement failure preserves previous comparisons and active
Evidence. A source absent from a successful discovery is marked missing and never
treated as permission to delete units. Restore an accidentally missing local original
file before consolidating its document-derived units, or explicitly retire those units.
Each changed source has a durable comparison showing current local units, complete
proposed content, supporting excerpts, and IDs that replacement would retire:
- **Keep local Evidence** retains the current wording as a manual declaration, with
its original documentary lineage preserved. The changed source is acknowledged;
the next unchanged refresh does not reopen that decision.
- **Use proposed Evidence** adopts the displayed proposal and explicitly retires the
displayed omitted IDs. The source version and provenance change together.
Both choices save and activate through the same local consolidation/index pipeline.
Review items block adoption; correct the original draft and refresh, or keep local
content. There is no automatic merge based on a model's semantic conflict assessment.
Any change to an affected curated file invalidates the comparison and requires a new
refresh. If indexing fails after the decision is saved, use **Retry saved decision**;
this reuses acquired content without fetching sources again. An intervening external
edit is never silently overwritten by recovery.
Headless operators can make the same decision using the source ID and comparison
revision from the local source registry or administration response:
```sh
tht --installation /absolute/path/thothii-installation.yaml \
workspace evidence decide --workspace psd-clinical \
--source-id <64-hex-source-id> --revision <64-hex-comparison-revision> \
--decision keep
```
Use `--decision replace` to adopt the proposal. All installed commands accept `--json`.
The Python `evidence sources` worker is internal to this installed command/API surface.
`evidence/.local/sources.json` stores source identities, comparisons and retry journals.
`evidence/.local/acquisitions/` preserves original acquired bytes and credential-free
remote provenance. Adopted normalized documents live under
`evidence/source/acquired/<source-id>/<content-hash>.md`. Versioned paths let a new
document and an older manual declaration's original source coexist. Include all of
these files in the existing manual Git/backup sequence. Do not edit managed acquired
versions: edit the original local draft or refresh its remote origin.
Deleted IDs remain reserved. Once a source has had a curated deletion, fresh model
IDs from that source are conservatively suppressed as well: changing an ID must not
restore retired knowledge. Existing surviving IDs can still receive reviewed updates;
deliberate new knowledge can be written as a manual Evidence file. Refresh is bounded
to 200 documents and 100 MiB per operation, in addition to each adapter's limits.
## Convert an existing workspace
The existing workflow CLI command `tht evidence migrate <workspace-root>` converts
unit versions 1–3 to 4 deterministically and initializes the archive baseline. It
preserves IDs, typed content, provenance and review items, with no model call or Git
commit. It does not activate a local index. Review items still block consolidation.
The command's existing Git-worktree path check remains in effect.
The first installed consolidation performs this conversion automatically when the
legacy manifest is present, then validates and activates the result. Preserve the
existing checkout in backups before upgrading. The E1 validation used an isolated
copy; E2 also converted and indexed all 35 units on the running local preview.
See the [E1 validation report](../plans/2026-09-09-evidence-e1-validation.md) and
[E2 validation report](../plans/2026-09-09-evidence-e2-validation.md).
+8 -5
View File
@@ -6,8 +6,9 @@ When present, `evidence` is strict: it contains `source` and a defaulted strict
source variant and the policy reject unknown keys.
The version numbers are intentionally separate: the Evidence descriptor supports v1/v2, while the
latest Curated Evidence Unit format is v3. There is no Evidence descriptor v3/v4 and no Curated
Evidence Unit v4.
latest Curated Evidence Unit format is [v4](curated-evidence-v4.md). There is no Evidence
descriptor v3/v4. The editable local archive is implemented in E1; installation integration
is the next increment, E2.
```mermaid
flowchart LR
@@ -67,9 +68,11 @@ ignore it. Domain rules also retain their exact canonical text in an invisible `
comment while presenting long prose as paragraphs, labelled subsections, and semicolon-derived
lists. Runtime chunking reads the parsed canonical rule, not this review-only presentation.
Newly prepared units use v3. `tht evidence migrate <workspace-root>` upgrades v1 and v2 units and
canonicalizes an older v3 presentation locally without a model call, commit, publication, or
semantic change.
The representations above are legacy conversion inputs. Newly prepared units use editable v4:
short YAML metadata, a visible H1 title and typed H2 payload fields, with no hidden content copy.
`tht evidence migrate <workspace-root>` converts v1–v3 to v4 and initializes a local archive
baseline without a model call, commit, activation, or semantic change. See the
[v4 editing and consolidation contract](curated-evidence-v4.md).
### Example: filesystem
+26 -7
View File
@@ -14,12 +14,30 @@ tht --installation <absolute>/thothii-installation.yaml workspace preprocess run
tht --installation <absolute>/thothii-installation.yaml workspace preprocess clear
--workspace <id> [--json]
tht --installation <absolute>/thothii-installation.yaml workspace evidence consolidate
--workspace <id> [--json]
tht --installation <absolute>/thothii-installation.yaml workspace evidence refresh
--workspace <id> [--json]
tht --installation <absolute>/thothii-installation.yaml workspace evidence decide
--workspace <id> --source-id <64-hex> --revision <64-hex>
--decision keep|replace [--json]
```
There are no public partial commands for DWH introspection, LSH, FK suggestions, schema indexing,
Evidence indexing, or Qdrant rebuild. `preprocess run` does not accept `--resume`, `--dry-run`, a
or Qdrant rebuild. The separate Evidence curation command validates editable local files and
activates only their index; it does not satisfy workspace preprocessing readiness or execute Git.
See [Curated Evidence v4](curated-evidence-v4.md#administration-and-installed-command).
Source refresh acquires and refines only on explicit request, saving comparisons without
changing active Evidence. Source decisions activate through the same Evidence-only
pipeline and preserve Catalog readiness. Their envelopes reject arbitrary URLs, paths
and extra flags; source locations and credentials come from installation configuration.
`preprocess run` does not accept `--resume`, `--dry-run`, a
generation identifier, or a rollback option. Re-running it replaces the preceding derived output.
`preprocess clear` removes all replaceable preprocessing output and preserves runtime memory.
`preprocess clear` removes all replaceable preprocessing output and preserves runtime memory
and the canonical local Evidence archive. Full preprocessing remains required afterward.
## Sources of truth
@@ -29,8 +47,10 @@ generation identifier, or a rollback option. Re-running it replaces the precedin
- PostgreSQL Metadata Catalog is the sole database authority. It owns the workspace/database
association, installation-local binding, tables, columns, descriptions, sensitivity flags,
physical foreign keys, and active logical relationships.
- The workspace Git revision remains authoritative for Evidence. Evidence Descriptor v1/v2 and
Curated Evidence Unit v3 are unchanged; there is no Evidence v4.
- Evidence Descriptor v1/v2 still configures the initial source. Initialized archives use
editable Curated Evidence Unit v4 and the last successfully activated local snapshot.
Ordinary preprocessing never imports unconsolidated working-tree edits. Before initialization,
the pinned workspace Git source remains supported.
No metadata is imported from legacy workspace YAML or `physical.yaml`/`annotations.yaml`.
@@ -98,9 +118,8 @@ generation produced for another database or revision.
`workspace preprocess clear` is intentionally narrower than deleting all semantic data. It:
1. copies any `memory` and `solved_question` records still present in the pre-split workspace
collection to `<workspace>-memory`, then retires that legacy collection (a normal preprocessing
write performs the same one-time cutover if clear was not invoked first);
1. leaves `<workspace>-memory` unchanged; Memory projections are reconstructed only from
the authoritative PostgreSQL archive, never imported from legacy vector payloads;
2. deletes `<workspace>-reference`;
3. removes the active LSH generation, Evidence corpus, private Catalog snapshot, and derived job
checkpoints;
+52 -3
View File
@@ -2,7 +2,30 @@
This page describes the complete workspace Evidence lifecycle: where original material lives, how curated units are produced, when they become available at runtime, and what the author and reviewer are responsible for.
## Publication rule
## Editable local Evidence
E1 adds [Curated Evidence v4 and a persistent local archive](contracts/curated-evidence-v4.md).
The visible title and payload fields are authoritative Markdown. New manual units need no
external source; consolidation records their curator and distinguishes later corrections
from original documentary provenance. The core archive API creates immutable candidates
and advances its active pointer only after successful indexing.
E2 adds **Administration → Evidence management**, actual host file paths, complete
browsing and filtering, and the installed `tht workspace evidence consolidate
--workspace <id>` command. Edit files externally, consolidate to activate them, then
review and run Git manually. Runtime consumes only the active local snapshot.
The [v4 contract](contracts/curated-evidence-v4.md#administration-and-installed-command)
describes host mounting, first conversion, failure recovery and Clear behavior.
The local PSD preview has 35 converted and indexed units. E3 adds explicit import/refresh
from local drafts and configured HTTP/S3 sources, durable current/proposed comparisons,
and keep/replace decisions with activation and retry. See
[Import drafts and refresh sources](contracts/curated-evidence-v4.md#import-drafts-and-refresh-sources).
Workflow gate corrections remain the subsequent shared increment, X1.
## Existing repository publication path
The remainder describes the legacy, uninitialized repository source path. Initialized
v4 local archives use the lifecycle above; manual declarations need no source document.
The workspace repository is the versioned source. ThothII reads it, validates it, and publishes an atomic generation. It does not modify, commit, or push the author's repository.
@@ -46,7 +69,33 @@ The legacy configuration may expose `source_root`, such as `${THT_DOCS_ROOT}` or
HTTP and S3 are separate adapters. They do not use the filesystem structure `source/` and `curated/`, but they must still provide stable provenance, without credentials in URIs, under the adapter-specific contract.
## What a curated unit must contain
## What an editable curated unit contains
New preparation produces unit schema v4. The first H1 contains its title; documented H2
sections contain the typed payload. A minimal manual unit is:
```markdown
---
schema_version: 4
id: evidence:order-key
kind: domain
language: en
purposes: [sql_generation]
---
# Order key
## Rule
Join orders using the order number, financial year and company.
```
Use `tht evidence migrate <workspace-root>` for deterministic legacy conversion. The
conversion preserves typed content and initializes an archive baseline; it does not
activate the local corpus. See the [v4 contract](contracts/curated-evidence-v4.md) for
all eight kinds, provenance, file layout and the E1/E2 boundary.
## Legacy v3 representation
Canonical Curated Evidence v3 hides canonical machine metadata in an HTML comment and renders the
whole review surface as real Markdown. GitHub therefore shows no frontmatter table. The body layout
@@ -94,7 +143,7 @@ La fascia pediatrica comprende i pazienti con età inferiore a 18 anni.
The actual files contain invisible `tht:` comments for canonical metadata and typed-field
boundaries. Removing, duplicating, or desynchronizing them makes validation fail closed instead of
silently ignoring content. Unit schemas v1 and v2 remain readable for compatibility, but newly
prepared units use v3.
prepared units use v4. Unit v3 remains readable as a conversion input.
Curated units must be atomic, readable by a second reviewer, and supported by the source.
Provenance references must lead back to the original file and the passage that supports the claim.
+85 -9
View File
@@ -8,14 +8,26 @@ declare providers, model allowlists, defaults, embeddings, dimensions, or vector
Do not edit `deploy/pi/models.json`, `deploy/pi/settings.json`, files under `generated/`, or
provider/model environment defaults. Those former sources are retired.
## Host instructions in Administration
The **Pi configuration** navigation button opens **Pi management**. Its Host maintenance
section selects the installation host's Linux, macOS, or Windows tab automatically; the
browser's operating system does not affect it. Selecting another tab is still possible.
The host CLI includes `THT_HOST_PLATFORM` in the generated Compose projection using the OS
on which it runs. Generate projections on the destination host, not on another computer,
and do not edit generated files. Without this projection (older deployments or native
development), the backend reports its own OS; a Linux Docker container cannot discover
whether the physical host is macOS or Windows. Run the updated host CLI's normal
configuration reload on the destination installation to regenerate this information.
## Minimal catalog
```yaml
schemaVersion: 2
modelCatalog:
defaults:
session: zai/glm-5.3
metadataGeneration: zai/glm-5.3
interaction: zai/glm-5.3
embedding:
id: ollama/qwen3-embedding:0.6b
@@ -50,23 +62,63 @@ it contains that use block:
- `metadataGeneration` makes it selectable for description generation;
- `embedding` is a single installation-level model rather than a selectable list.
`defaults.session` is required. `defaults.metadataGeneration` is required exactly when at least
one metadata-generation model exists. A session manifest pins its canonical identity, so removing a
model never silently changes an existing session: resume fails with `model_unavailable`.
`defaults.interaction` is the only LLM default, required once per installation, never per workspace.
Core and Administration share the user's operational model choice. An explicit choice takes priority
over the default and is remembered in this browser for the authenticated user and application mount.
Switching workspace does not change the model. Browser-storage restrictions may limit remembering
to the current visit; preferences do not synchronize across devices or change installation YAML.
An unavailable remembered model is not silently replaced: select another configured model.
When any metadata-generation models are configured, the default and the operational list must
support both `session` and `metadataGeneration`. Entries for just one adapter may remain in the
installation inventory, but are not selectable for global interaction. Core-only installations remain
supported when no metadata-generation model is configured; Admin AI is then unavailable.
The embedding model remains separate and is unaffected by the interaction selector.
New sessions record the selected model in their manifest. Resume retains the session's workspace and
revision, but uses the current global model (the installation default for clients that omit a model).
Historical manifest model fields are not rewritten by resume. Archived/finalized sessions remain read-only.
### Required operator verification for every model
Catalog validation checks configuration, not model behavior. Before offering a model to users, and
after changing its endpoint, adapters, or the Pi/LiteLLM versions, the operator must verify **both**:
1. **Core / Pi:** select the model, start a test session in a prepared test workspace, exercise an
actual tool call and its returned result, a human review gate, and stop/resume. Check streaming,
tool arguments, authentication, and reasoning/token-limit compatibility. A plain chat reply or
`tht pi test` alone is not sufficient.
2. **Administration / LiteLLM:** select the same model and generate descriptions for a small,
non-sensitive test table. Check the structured result is accepted and the generation completes.
Review the output quality before using it on real metadata. This action writes test metadata
and may incur provider charges: use an authorized test database and approved data.
There is no automatic certification flag or startup model probe. The operator owns this verification;
do not infer compatibility from the model label or from success in just one path. Both adapters point
to one catalog identity; Pi does not need to route through a new LiteLLM proxy. Models using only
`pi_auth` cannot serve the current LiteLLM path and are excluded from shared selection.
## Session adapters
Use `pi_builtin` for a model whose technical definition ships with Pi:
Use `pi_builtin` for a model whose technical definition ships with Pi. This does not require
`pi_auth`: a shared bundle credential lets native Pi and LiteLLM use the same provider identity:
```yaml
deepseek:
authentication:
mode: pi_auth
mode: secret_env
apiKeyEnv: DEEPSEEK_API_KEY
session:
mode: pi_builtin
metadataGeneration:
litellmProvider: deepseek
models:
deepseek-v4-pro:
session: {}
metadataGeneration: {}
deepseek-v4-flash:
session: {}
metadataGeneration: {}
```
Use `openai_compatible` for an explicit compatible endpoint. Each eligible session model must then
@@ -89,6 +141,18 @@ Secret values never belong in installation YAML, generated files, logs, CLI argu
requests. The YAML contains only an environment-variable name or an authentication mode. Pi's
protected credential file remains selected by the installation authentication configuration.
For catalog providers using `secret_env`, the bundle is authoritative in both Core and Admin.
ThothII removes only the selected provider's old auth entry from the temporary Pi session snapshot;
the operator's original Pi auth store and other providers are unchanged. Provider smoke checks use
the same precedence, and model enumeration receives the catalog-declared bundle keys. A missing
declared key is an error, not permission to fall back to Pi auth or the legacy generic key file.
After rotating a bundle key, apply the normal installation lifecycle so processes reload it.
The PSD descriptor now declares only `deepseek/deepseek-v4-pro` and `deepseek/deepseek-v4-flash`
for both uses; it no longer duplicates them under `deepseek-metadata`. Historical records are not
rewritten. A saved obsolete identity must be explicitly reselected from the current catalog;
it is not silently remapped to another model or account.
## Generated runtime projections
Before Compose starts, `tht` validates the installation and atomically writes deterministic files
@@ -133,6 +197,16 @@ configuration command. There is no `tht pi configure` and no separate apply comm
## Migrating a legacy installation
For schema-v2 descriptors with the former `defaults.session` and `defaults.metadataGeneration`,
replace both with `defaults.interaction`. Equal legacy values are accepted and normalized in memory;
the loader never rewrites the descriptor. Different values fail with `migration_required`: explicitly
choose a model supporting both uses, remove both old fields, and set the single new field. Do not mix
new and legacy fields. A Core-only legacy session default can be normalized when Admin AI is absent.
The generated runtime catalog now uses schema version **2** and only `defaultInteraction`. Regenerate
and apply all runtime projections with the matching host/backend release using the normal installation
lifecycle; do not deploy only the backend against an old generated catalog or hand-edit generated JSON.
The migrator reads the former installation `metadataGeneration` block and the two former Pi JSON
files, but never modifies them. Supply the facts that cannot be inferred safely and write a separate
candidate:
@@ -148,13 +222,15 @@ tht --installation /absolute/path/legacy/thothii-installation.yaml installation
Review the candidate, move the legacy source files out of the installation only after approval,
then select the v2 descriptor. Ambiguous aliases, endpoint conflicts, or missing authentication
facts produce field-level errors; the migrator does not guess.
The legacy CLI flag `--session-default` now supplies the unified interaction default in the candidate;
if it conflicts with the legacy metadata default, align that choice explicitly before retrying.
## Troubleshooting
| Symptom | Meaning | Action |
| --- | --- | --- |
| `migration_required` | A retired model source or installation schema is still present | Run the installation migrator and review its candidate |
| Unknown session or metadata default | The canonical ID is missing the corresponding use block | Correct the provider/model key or add the intended use block |
| Invalid interaction default | The canonical ID does not support all configured uses | Correct `defaults.interaction` or the intended adapter blocks |
| Generated projection drift | Runtime files differ from the descriptor-derived bytes | Run `tht start` or `tht pi restart --yes --drain` |
| `model_unavailable` on resume | The session's pinned model is no longer session-eligible | Restore that catalog entry or keep the session unavailable; do not remap it |
| `model_unavailable` on create/resume | The selected global model is no longer eligible | Explicitly choose an eligible model; no fallback is applied |
| Provider smoke failure | Credentials, endpoint, or provider availability is invalid | Correct the protected credential or catalog endpoint, restart, then run `tht pi test` |
+208 -222
View File
@@ -1,244 +1,230 @@
# Memory management
This document describes how Memory currently works in ThothII: its conceptual model, lifecycle, persistence, semantic search, review gates, session display, and main technical limits.
Memory holds reusable knowledge for a workspace. PostgreSQL is the authoritative
archive; Qdrant contains a rebuildable search projection. The harness owns both
the administrative operations and the verification of retrieved results.
## Architectural summary
## Administration
A Memory item is domain knowledge that can be reused across questions. It is not a copy of one question's schema linking.
Open **Administration → Memory management**, immediately after Database management.
An administrator selects the workspace explicitly. No active session, DWH binding,
embedding service or Qdrant connection is required to browse and edit the archive.
PostgreSQL must be available.
The page provides a paginated list, text/ID search, stable sorting, and combined
filters for family, concepts, database, table, column, origin and update date.
Filtering happens across the entire archive before pagination. Each card has a
complete detail view, an editable form, cancellation of unsaved changes, and an
explicit deletion confirmation.
| Family | Content |
| --- | --- |
| Domain clarification | A reusable definition or interpretation, with scope and context. |
| SQL rule | Guidance for constructing SQL, with scope and rationale. |
| Solved question | A question, its approved SQL and context; used as a consultative exemplar. |
| Explained error | A correction and its rationale, to avoid repeating a known error. |
All cards have a stable `mem-<UUID>` identity, title, scope, origin and timestamps.
Manual cards have no invented source session or decision. Workflow cards retain
their source references when an administrator edits them. There is no editorial
revision history.
The form also manages concepts, structured database/schema/table/column
dependencies, and links to other cards with an explicit meaning. Links can only
connect cards in the same workspace. Card, dependency and outgoing-link changes
are committed together. Deleting a card removes its incident links and dependencies,
while keeping the other cards, Evidence and Catalog metadata.
## Save, failure and recovery
```mermaid
flowchart TB
CLARIFY["F1 concept clarified"] --> REVIEW["F8 reviewer review"]
REVIEW -->|"accepted"| REGISTRY["registry.jsonl"]
REVIEW -->|"declined"| LOCAL["Session decision only"]
REGISTRY --> VECTOR["Qdrant semantic index"]
VECTOR --> FUTURE["Future F2 retrieval"]
FUTURE --> PROPOSAL["Reviewer proposal"]
flowchart LR
EDIT["Admin or workflow"] --> SQL["PostgreSQL transaction"]
SQL --> CARD["Current card and links"]
SQL --> WORK["Pending projection"]
WORK --> Q["Qdrant"]
Q --> CHECK["Verify current card and projection"]
CARD --> CHECK
CHECK --> REVIEW["Recall for human review"]
```
```text
F1: clarify a concept
│
▼
concept_clarified decision in the session ledger
│
▼
F8: reviewer decides whether to promote it
│
├── global registry registry.jsonl
└── Qdrant semantic index
│
▼
F2 in a future session
search and proposal to the reviewer
```
The service serializes each workspace's mutations. It first commits content and
the projection operation in PostgreSQL, then propagates to Qdrant. A failed save
is different from **saved, index update incomplete**. In the latter case the
current content is already available in administration, while its previous vector
result is excluded from recall.
The main invariant is `REUSABLE_TYPES = {"concept_clarified"}`: only clarified concepts can be generated, saved, searched, or proposed as Memory. Decisions such as `table_promoted`, `table_excluded`, and `column_promoted` remain local to the question.
The **Pending index updates** section offers an explicit retry, including cleanup
for deleted cards. These operations survive a restart. Retry reads the current
archive state and cannot restore an earlier edit or a deleted card. Technical
revisions are internal consistency markers, not user-managed card statuses.
## The three management levels
Every recall hit must resolve to a current card in the requested workspace, with
a matching valid projection. The response is reconstructed from PostgreSQL, never
from an unverified Qdrant payload. Deleted, orphaned and stale points are excluded
for both domain Memory and solved-question exemplars. An unavailable archive is
an operational error, not a successful empty archive.
| Level | Content | Function |
Rebuilding Memory uses only authoritative cards. It does not import JSONL files,
historical session artifacts or old vector payloads. Reference preprocessing keeps
the Memory collection separate and does not migrate legacy Memory payloads.
## Hybrid recall and links
Memory uses dense embeddings and Qdrant BM25, fused with reciprocal rank fusion.
Both branches receive the same workspace, family and scope filters before candidate
selection. Indexed text includes content, scope, rationale, question, exemplar SQL,
concepts and qualified physical dependencies. Both indexing and querying use the
workspace language (`en` or `it`), including manual administration without a DWH binding.
The workflow CLI binds recall to its configured database and schema. Optional
`--filters` JSON can narrow the business `scope`, `concepts`, `table` and `column`.
Business scope is an exact string; every requested concept must be present. Physical
context matches a single structured dependency: database, schema, table and column
cannot be satisfied by unrelated entries. A card without dependencies is workspace-wide;
a database-only or table-only dependency also applies to descendants. Without a table
filter, table-specific knowledge in the selected database/schema remains discoverable.
Filters cannot override the configured database/schema. Free-text business scope is
not automatically interpreted or inferred from the question.
The core expands outgoing links from current, eligible search candidates. It applies
the same filters and workflow-family restrictions to destinations and intermediate
cards. Removed, pending, previously decided and out-of-scope cards cannot act as bridges.
Traversal allows two hops, at most 20 outgoing links per card (stable target-ID order),
200 distinct card lookups and 400 inspected links in total. Search requests
`min(100, max(20, 3 × top))` seeds; the result limit remains between 1 and 100.
All candidates are ranked together using `1 / (60 + seed rank)` for direct hits,
plus the strongest linked contribution, decayed by `0.5` per hop. Repeated paths
do not accumulate votes. Ties use card ID. The returned `score` is a ranking score,
not cosine similarity or a confidence estimate. `retrieval.path` shows the strongest
link path, or just the card ID for an exclusively direct result. PostgreSQL content,
links and eligibility are resolved under the workspace operation lock after search.
Migration `002_hybrid_projection.sql` marks the format of old dense projections as
incompatible without changing their authoritative cards. They appear in pending
updates and are excluded from recall until explicit retry or `memory index` succeeds.
Memory writes can add a missing BM25 sparse vector to their collection; an incompatible
existing vector configuration fails visibly and leaves recovery pending. Reading never
silently falls back to dense retrieval. Reference remains independently managed.
An explicit `memory index` can also recreate a missing Memory collection before
rebuilding its projections; it never imports records from another source.
## Workflow integration
During the workflow, Pi prepares reusable proposals in the session artifact
`memory_proposals.json`. Each proposal names effective approved source decisions,
the content and scope, its rationale, and any physical dependencies or links.
Unexplained failures, rejected options and simple table selections do not create
reusable knowledge. Exact existing cards are reused; semantic similarity alone
never authorizes replacement. Updates name the existing card and its current revision.
At F8, `reviewer_memory_promote` presents one editable Memory summary, including
the approved solved question. The reviewer chooses additions and updates, edits
their content, scope, dependencies and links, or declines everything. Approved SQL
is read only here: changing the solution requires returning to SQL review.
Only selected cards and their links are committed. Invalid links or stale updates
roll back the entire selection. Links between selected new cards are resolved
inside the same transaction. An explicit update preserves the existing identity
and origin, including manually authored content.
A durable review receipt makes repeated delivery idempotent and recovers the
gap between saving Memory and recording `memory_summary_reviewed` in the session
ledger. Retry never recreates a deleted card. The gate then closes F8 and finalizes;
finalization itself performs no automatic Memory writes. Pending indexing remains
visible and recoverable in administration.
F2 consumes domain clarifications and excludes already decided Memory. In F4,
F6 and F7, `memory rules` retrieves applicable SQL rules and explained errors.
Pi presents their use in the existing schema, CTE or SQL approval gate.
Exemplar search remains consultative. Retrieval never constitutes approval.
Persistent Memory/Evidence conflict repair remains part of the joint X1 increment.
## Physical schema changes
After a successful physical Catalog synchronization, the backend passes the exact
removed tables and columns, database, schema and run identity to Memory. Only cards
with matching structured dependencies are deleted, together with their incident
links and searchable projections. Global cards and objects outside the synchronized
scope survive. Manual Catalog cleanup and failed DWH scans never trigger this deletion.
The Catalog transaction records a pending `memory_cleanup` phase before committing
the physical change. If cleanup or indexing fails, the run retains the original
removals. Retry completes that same operation without rescanning the DWH or relying
on a new diff. A new synchronization is blocked until this cleanup is completed.
The Memory deletion receipt and vector tombstones make the operation repeatable
across restarts. The synchronization drawer reports deleted Memory cards and errors.
## API and commands
The administrative HTTP surface requires `memory.manage`, included in the existing
admin role. The backend checks workspace identity and passes its trusted principal
and a protected request snapshot to the harness. The harness independently checks
the principal; ordinary users cannot bypass administration through the CLI.
Production authentication and CSRF protections apply to the new routes.
| Method | Path below `/api/workspaces/:workspaceId/memory` | Operation |
| --- | --- | --- |
| Session ledger | `concept_clarified`, `memory_promoted`, `memory_promotion_declined` | Audit and state for one session |
| Global registry | `mem-XXXX` records in `registry.jsonl` | Current canonical Memory archive |
| Qdrant index | Embeddings and metadata derived from the registry | Semantic search |
| GET | root | Search, filter and paginate the archive |
| POST | root | Create a card |
| GET | `/:cardId` | Read a complete card |
| PUT | `/:cardId` | Save content, links and dependencies together |
| DELETE | `/:cardId` | Delete a card and incident links |
| GET | `/pending` | List incomplete projection operations |
| POST | `/:cardId/retry` | Retry current projection work, including deletion |
The ledger contains provenance and human decisions. The global record contains reusable text. The vector index is a search projection, not the place where the workflow records decisions directly.
## What can become Memory
During F1, the workflow records clarifications as `concept_clarified` decisions. A clarification can express:
- definitions of production or organizational concepts;
- inclusion and exclusion criteria for a product line;
- formulas and calculation methods;
- interpretations of time periods;
- mappings to specific tables and columns;
- the meaning of flags, codes, or indicators.
The `MemoryRecord` model contains:
- `id`, such as `mem-0001`;
- the timestamp, session, and sequence of the original decision;
- `type`;
- `subject`;
- `detail`;
- `rationale`;
- `question_context`;
- `tables` e `concepts`.
For new Memory items, the type is always `concept_clarified` and `tables` starts empty. A table or column may appear in the explanation as a technical mapping, but it cannot be the Memory item's standalone concept.
Valid example:
> Ablation means a procedure with `ablazione_transcatetere = TRUE`, counted with `COUNT(DISTINCT cod_paz)` by year.
Invalid examples:
- `fact_cardioversione` as approved Memory;
- `dim_time` as rejected Memory;
- an "include this table" decision saved for future questions.
The workflow skill also documents this rule in [harness/.pi/skills/tht-sessione/SKILL.md](https://git.tylconsulting.it/mptyl/ThothII/src/branch/main/harness/.pi/skills/tht-sessione/SKILL.md#L217).
## Promotion at the end of the session: F8
At the end of the workflow, the `reviewer_memory_promote` gate runs a deterministic preview:
CLI configuration remains a per-command option. Commands emit pure JSON:
```text
tht memory promote --session <id> --preview --json
tht memory list --filters '{"family":"sql_rule","page":1}' -c <runtime.yaml>
tht memory show <card-id> -c <runtime.yaml>
tht memory create --data <card.json> -c <runtime.yaml>
tht memory update <card-id> --data <card.json> -c <runtime.yaml>
tht memory delete <card-id> --yes -c <runtime.yaml>
tht memory pending -c <runtime.yaml>
tht memory retry <card-id> -c <runtime.yaml>
tht memory index -c <runtime.yaml>
tht memory search "<question>" --session <id> --json -c <runtime.yaml>
tht memory search "<question>" --filters '{"table":"orders","column":"id","scope":"Sales"}' --json -c <runtime.yaml>
tht memory solved-search "<question>" --json -c <runtime.yaml>
tht memory rules "<question>" --session <id> --json -c <runtime.yaml>
tht memory propose --session <id> --data <proposals.json> -c <runtime.yaml>
tht memory summary --session <id> --json -c <runtime.yaml>
tht memory solved-index <session-id> --json -c <runtime.yaml>
```
The preview:
`memory index` rebuilds both domain and solved-question projections.
`solved-index` only retries an existing authoritative source receipt.
The reviewer gate owns `memory review-apply`; Pi must not call it directly.
Migration `003_review_receipts.sql` adds durable review and physical-cleanup receipts.
The backend uses `memory admin --workspace <id> -c <protected-request.json>` so
administration does not materialize session or DWH configuration.
1. reads the session's effective decisions;
2. considers only `concept_clarified`;
3. discards decisions already promoted;
4. discards sequences already declined in F8;
5. deduplicates equivalent content;
6. proposes at most five candidates.
## Installation and storage
The code applies filtering and deduplication in
[harness/tht/memory/core.py](https://git.tylconsulting.it/mptyl/ThothII/src/branch/main/harness/tht/memory/core.py); the gate applies an additional defensive filter in
[harness/.pi/extensions/gate/memory/index.js](https://git.tylconsulting.it/mptyl/ThothII/src/branch/main/harness/.pi/extensions/gate/memory/index.js).
Memory shares the installation's existing PostgreSQL service, using its own
`thoth_memory` schema and versioned harness migration pack. The existing
`catalog-migrate` preparation service runs Catalog migrations followed by
`python -m tht.memory.migrate`. The core image includes both migration runners.
Ordinary API requests never migrate the schema.
The reviewer sees one preselected checklist. For each candidate:
Connection credentials come from the existing generated `THT_CATALOG_DB_HOST`,
`THT_CATALOG_DB_PORT`, `THT_CATALOG_DB_NAME`, `THT_CATALOG_RUNTIME_USER` and
`THT_CATALOG_RUNTIME_PASSWORD_FILE`. Migration uses the corresponding migrator
user/password file. Direct URL environments can use `THT_CATALOG_DATABASE_URL`
(runtime) and `THT_CATALOG_MIGRATOR_DATABASE_URL`; the harness also accepts
`THT_CATALOG_RUNTIME_DATABASE_URL`. Credentials are not authored in workspace YAML.
- selected: `tht memory save-one` runs, followed by a `memory_promoted` record;
- deselected: records `memory_promotion_declined`;
- no candidates: F8 closes automatically.
The migration grants the installation login membership in the restricted
`thoth_memory_runtime` role. Every repository transaction sets that role and a
workspace context. Forced row-level policies isolate cards, links, dependencies
and projection operations. This role has Memory DML and migration-status read
access, with no runtime DDL privilege. There are no cascading foreign keys to
Catalog or Evidence. A missing or incompatible schema returns a clear operational
error. Preparation is repeatable and checks migration checksums.
The `memory_promoted` marker uses `detail: seq:N`, a reference to the original `concept_clarified` decision. The flow is in [harness/.pi/extensions/tht-gate.js](https://git.tylconsulting.it/mptyl/ThothII/src/branch/main/harness/.pi/extensions/tht-gate.js#L1691).
Promotion does not run in F2, and the model cannot invent F8 candidates. Direct promotion commands are also protected by the anti-bypass gate.
## Global persistence
### JSONL registry
The current registry is:
```text
<artifacts>/memory/registry.jsonl
```
The registry is written through a temporary file and `os.replace`, so replacement is atomic. Promotion is idempotent on the `session_id + decision_seq` pair: the same decision from the same session cannot create two global records.
### Qdrant
After promotion, `save-one` builds one `VectorRecord` and sends it to the Qdrant index. The indexed text includes:
- type and subject;
- detail;
- rationale;
- question context;
- any concepts and mappings.
The vector record uses the ID `memory:mem-XXXX`. Its metadata stores `subject`, `detail`, `rationale`, `tables`, `concepts`, and the `kind` discriminator. The content's SHA-256 hash prevents embedding and upsert work when the text has not changed.
This behavior is implemented in
[harness/tht/memory/core.py](https://git.tylconsulting.it/mptyl/ThothII/src/branch/main/harness/tht/memory/core.py).
### Current canonical source
The JSONL registry remains the application's canonical source, while Qdrant is a derived but persistent index. The workflow does not record decisions directly in the vector database. It uses Qdrant as a searchable projection of the registry and effective ledger.
## Reuse in F2
In a future session, F2 runs:
```text
tht memory search "<question>" --session <id> --json
```
The command:
1. creates an embedding for the question;
2. searches the vector store for records with `kind=memory` only;
3. resolves each hit in the JSONL registry through its `ref`;
4. discards records missing from the registry;
5. discards every type other than `concept_clarified`;
6. excludes Memory already decided in the current session;
7. returns results ordered by similarity.
Memory is never applied automatically. The model must present it in one `reviewer_decide` choice:
- a selected Memory item is recorded as a new `concept_clarified` in the current session;
- the rationale must cite the original `mem-XXXX` ID;
- a deselected Memory item means "do not apply it now", not "delete it globally".
If F2 is reopened, a deselected Memory item can be proposed again. `memory_rejected` remains supported for legacy decisions and sessions, but it is not the normal behavior for current F2 deselection.
## Effective ledger, rollback, and reopening
The ledger is append-only. Reopening and withdrawing decisions do not delete earlier rows, but they change which decisions are effective.
Memory helpers use `effective_decisions()` to:
- exclude withdrawn decisions;
- ignore decisions from phases that became stale after a rollback;
- prevent promotion of clarifications that are no longer valid.
The effective view is defined in [harness/tht/phase.py](https://git.tylconsulting.it/mptyl/ThothII/src/branch/main/harness/tht/phase.py#L82).
## Display in the session summary
The "Memories" section of the summary is projected at runtime from the session ledger; it is not a direct copy of the global registry.
The projection:
- shows approved Memory first;
- then shows declined Memory;
- resolves `seq:N` to the original `concept_clarified`;
- hides markers whose original record is not `concept_clarified`;
- hides subjects that match schema-linking tables;
- hides standalone subjects shaped like `fact_*` or `dim_*`;
- keeps table and field references when they are part of the conceptual explanation.
The logic is in [harness/tht/session/store.py](https://git.tylconsulting.it/mptyl/ThothII/src/branch/main/harness/tht/session/store.py#L237). The frontend renders a structured list and treats `subject`, `detail`, and `rationale` as Markdown instead of showing raw Markdown.
The transformation happens on read. Historical sessions use the current layout and filters without rewriting their original artifacts.
## Enforced invariants
The protections are distributed across several boundaries:
1. `REUSABLE_TYPES` in the Python core;
2. the `memory search` command filter;
3. the F8 preview filter;
4. filtering and deduplication in the Pi gate;
5. exclusion of table Memory from the UI projection.
This prevents one prompt or component change from reintroducing tables as Memory.
## Remaining limits and risks
### The registry and index are not one transaction
Saving broadly follows this sequence:
```text
JSONL registry → Qdrant → memory_promoted marker in the ledger
```
If Qdrant is unavailable, the registry can contain Memory that is not yet searchable. The command reports that reindexing is required.
If the ledger marker fails after the vector database save, the Memory item can exist globally without a complete session audit. The gate returns a manual recovery command.
### Five-candidate limit
F8 proposes at most five Memory items. If a session produces more than five valid concepts, the extra items are not shown and the session can be finalized without promoting them.
### Deduplication is not global
Deduplication prevents duplicates within one proposal, and idempotency prevents the same decision from being promoted twice. There is no global merge of semantically similar Memory items from different sessions.
### Old physical records
Old `table_promoted` or `table_excluded` records may still exist in historical artifacts or indexes. The current code makes them unusable by filtering by type and does not show them in session projections. Physically removing them from the vector database remains a separate cleanup task.
## Final assessment
The current implementation matches the functional requirement: Memory is reusable conceptual knowledge, not a schema-linking choice.
The strongest part is the multilayer protection of the `concept_clarified` type. The main technical debt is the coexistence of the JSONL registry and Qdrant, with no single transaction spanning the global archive, semantic index, and session ledger.
See [the M1 specification](plans/2026-09-08-memory-m1-spec.md) and
[ADR 0018](adr/0018-use-postgres-for-memory-and-qdrant-for-retrieval.md) for the
approved scope and acceptance boundaries.
See [M2 implementation and validation](plans/2026-09-09-memory-m2-validation.md)
for the retrieval checks and real embedding test command.
@@ -0,0 +1,492 @@
# Progetto: Evidence management
Data: 2026-09-08. Stato: archivio locale Markdown, editor esterni e consolidamento
manuale con commit/push dell'operatore accettati per la release 0;
restanti semplificazioni confermate, con chiarimenti su impatto core e controllo Git;
E1, E2, E3 e il collegamento ai gate X1 implementati il 2026-09-09.
Il contratto X1 è in [Session corrections](../contracts/archive-repair.md). Risultati e limiti in
[E1 — validazione](2026-09-09-evidence-e1-validation.md) e
[E2 — validazione](2026-09-09-evidence-e2-validation.md) e
[E3 — validazione](2026-09-09-evidence-e3-validation.md).
La [revisione di semplicità](2026-09-08-memory-evidence-simplification-review.md)
registra il riesame di Q1–Q15. Il flusso principale è draft scritta dallo specialista
indipendentemente dall'installazione, raffinamento da parte del sistema e
conservazione locale per uso e manutenzione. La proposta tecnica aggiornata
distingue le fonti esterne dall'archivio canonico locale, evita PostgreSQL per le
Evidence e toglie commit/push dal normale CRUD. La contemporaneità con attività core
durante l'amministrazione non è più un requisito da supportare.
Il progetto rende consultabili le Evidence dall'applicazione e modificabili nei
file locali tramite l'editor scelto dall'operatore.
Segue le [decisioni comuni di Administration](2026-09-08-memory-evidence-administration.md)
ed è consegnabile separatamente da [Memory management](2026-09-08-memory-management.md).
## Risultato richiesto
Un amministratore apre Evidence management direttamente da Administration, cerca e
filtra le Evidence registrate, consulta il documento completo e trova il percorso
del file Markdown da modificare con un editor esterno. La voce segue Memory management e si trova allo stesso
livello di Database management, senza appartenere alle sue funzionalità.
La consultazione è indipendente da una sessione e dalla disponibilità dell'indice
semantico. Mostra le Evidence Unit complete, senza limitarsi ai frammenti o ai
risultati più simili restituiti dal recall.
## Accesso e modifica nella release 0
Il proprietario richiede file facilmente raggiungibili e modificabili anche da uno
specialista non tecnico, con Markdown come formato di lavoro e senza JSONL per la
gestione delle Evidence. Ha scelto editor esterni per la release 0: l'editor
preferito sul Mac o PC, oppure vim, nano o equivalenti sul server. Questa scelta
sostituisce la proposta di editor e form di contenuto dentro Evidence management.
Non si integra un prodotto di editing né si costruisce un editor applicativo.
Evidence management mantiene elenco, ricerca, filtri e dettaglio. Mostra il percorso
assoluto della cartella del workspace e di ciascun file, con possibilità di copiarlo,
risolto dalla configurazione effettiva dell'installazione. Indica su quale host si
trova: per Docker occorre il percorso persistente accessibile sull'host, non soltanto
quello interno al container. Le istruzioni distinguono draft originali e file delle
Evidence raffinate da manutenere; includono un esempio Markdown per crearne una.
Chi lavora sul server modifica direttamente i file; chi lavora su una copia sul
Mac o PC la riporta nello stesso archivio con i propri strumenti. Non è richiesto
costruire upload/download nell'applicazione o predisporre una cartella condivisa.
Un percorso sul server non viene presentato come un file apribile dal browser locale.
Dopo aver salvato, aggiunto o rimosso i file, l'operatore controlla lo stato Git,
esegue il consolidamento ed esegue commit e push; il normale diff Git è disponibile
per approfondire le modifiche. La pagina mostra
le istruzioni e i comandi con i percorsi effettivi. La richiesta più recente
sostituisce la proposta intermedia del pulsante `Apply file changes`: non sono
necessari un pulsante di esecuzione, watcher o operazioni Git automatiche.
Il semplice salvataggio nell'editor, o di una copia fuori dall'archivio, non aggiorna
il recall. Il consolidamento e il seguito Git sono descritti sotto.
Il Markdown canonico attuale non è già un formato di editing libero: il parser
controlla marker e corrispondenza fra testo canonico e presentazione. Per le regole
di dominio il testo codificato viene letto prima di verificare il rendering; cambiare
la sola frase visibile può produrre un errore di canonicalità. E1 deve quindi rendere
il Markdown locale realmente editabile: il testo leggibile è autorevole, i campi
richiesti sono documentati e i metadati derivati sono aggiornati dal sistema.
Non si chiede all'operatore di correggere marker, hash o una copia codificata del
testo. Non si introduce un secondo archivio di scambio da sincronizzare.
## Funzioni
- Elenco paginato e ordinabile; ricerca per testo, titolo e identificatore stabile.
- Filtri combinabili per workspace, kind, purpose, concetti, tabelle/colonne,
Source Evidence, Review item e stato di pubblicazione.
- Dettaglio leggibile della card: contenuto tipizzato, ambito di applicazione,
Supporting excerpt, provenienza e accesso alla Source Evidence disponibile.
- Creazione e modifica tramite file Markdown ed editor esterno, con esempio dei
campi richiesti per kind. In assenza di documento esterno, l'applicazione registra
una dichiarazione manuale come fonte quando acquisisce la modifica.
- Risoluzione dei Review item e gestione delle unità orfane o da ritirare.
- Cancellazione tramite rimozione del file, riconosciuta dal consolidamento
nell'archivio locale verificato come accessibile; un errore di lettura non prova
una cancellazione. La rimozione dal corpus e dall'indice è persistente.
- Un comando manuale di consolidamento per acquisire e validare le modifiche locali
e renderle disponibili al core, con esito esplicito e possibilità di riesecuzione.
- Istruzioni per controllo del diff, commit e push dei file Evidence e dei relativi
metadati necessari, eseguiti dall'operatore nel repository indicato.
Il servizio acquisisce il Markdown editabile e aggiorna la rappresentazione
tipizzata e i metadati derivati, preservando gli identificatori delle unità esistenti.
La cancellazione di una Evidence Unit non elimina automaticamente la sua Source
Evidence o altre unità derivate dalla stessa fonte.
## Salvataggio — Q12 semplificata
Il proprietario rifiuta la separazione ordinaria fra salvataggio della bozza e
pubblicazione: le modifiche durante una sessione sono considerate rare e non
giustificano un workflow editoriale separato. La scelta successiva dell'editor esterno
sostituisce il `Save` della form con il salvataggio del file e un comando manuale
di consolidamento. Questo acquisisce, valida e attiva insieme aggiunte, modifiche e
cancellazioni, senza un successivo `Publish` o un secondo revisore. Commit e push
sono il seguito manuale per versionare e trasferire il lavoro nel repository.
L'operatore autorizza l'attivazione con il consolidamento. Le correzioni approvate nei
gate chiamano direttamente lo stesso servizio e non richiedono un editor esterno.
L'ultimo chiarimento del proprietario sostituisce il requisito iniziale di aggiornare
le sessioni aperte mentre un amministratore modifica le Evidence. Si assume che il
core sia fermo o si ignora la contemporaneità: non servono aggiornamenti a caldo,
notifiche, ricalcoli, attese delle sessioni o una modalità manutenzione dedicata.
Il risultato di una modifica completata è usato nelle successive elaborazioni.
Restano le correzioni deliberate dalla sessione stessa secondo Q4: chiamano lo
stesso servizio, attendono l'esito e permettono alla sessione di usare la correzione
approvata. Il collegamento fra configurazione della ricerca e contenuto locale
deve quindi funzionare senza richiedere un nuovo commit della fonte per ogni modifica;
non richiede un sistema generale di aggiornamento delle altre sessioni.
Nel seguito, salvataggio applicativo indica questa acquisizione delle modifiche o
la scrittura deliberata dal core. Il feedback ordinario è operazione in corso,
completata oppure errore. Un successo
completo significa che il contenuto è disponibile al core; se la persistenza riesce
ma l'aggiornamento del corpus o dell'indice fallisce, l'esito deve dirlo e indicare
come rieseguire il comando. Un errore tecnico non diventa una bozza che attende una nuova decisione
editoriale di pubblicazione. Dopo un errore o un riavvio, un corpus parziale non
deve essere dichiarato pronto per la successiva elaborazione.
Le decisioni Q11–Q15 e il successivo chiarimento sui vincoli sono registrati
nell'[ADR 0019](../adr/0019-author-evidence-in-app-with-automatic-activation.md).
## Consolidamento manuale e seguito Git — release 0 {#consolidamento-manuale-e-seguito-git--release-0}
Il proprietario richiede un flusso KISS affidato alla disciplina dell'operatore.
Il comando proposto è `tht evidence consolidate <workspace-root>` nel harness:
è da implementare, non è un comando già disponibile. Riutilizza parser, validazione
e indicizzazione esistenti, adattati al Markdown editabile. La documentazione
operativa e la pagina mostreranno l'invocazione effettiva per l'installazione,
compreso l'accesso al core se il comando gira in Docker.
Il consolidamento svolge in sequenza questi passaggi:
1. Legge l'archivio locale e confronta aggiunte, modifiche e rimozioni con l'ultimo
contenuto consolidato. Se l'archivio non è accessibile, si ferma: non interpreta
il problema come cancellazione delle Evidence.
2. Verifica struttura e sezioni previste per kind, campi obbligatori, identificatori
univoci, tipi, riferimenti e provenienza coerenti. Segnala file, campo o sezione,
problema e correzione richiesta. Non valuta automaticamente la verità del contenuto
e non riscrive il significato delle regole con un nuovo raffinamento del modello.
3. Se ci sono errori, termina con esito non riuscito prima dell'attivazione; lascia
all'operatore i file da correggere e il comando da rieseguire. Se i controlli
passano, aggiorna metadati derivati e manifest, inclusa la protezione delle
correzioni e delle cancellazioni, poi prepara e verifica il candidato completo
prima di renderlo attivo nel corpus e nell'indice Evidence locale.
4. Riporta un riepilogo testuale breve di file aggiunti, modificati e cancellati,
l'esito locale, gli eventuali errori di indicizzazione e i file primari
da includere nel commit. Dopo un errore tecnico si riesegue lo stesso comando,
senza duplicare unità o perdere il contenuto modificato. Non dichiara pronto un
aggiornamento incompleto e non avvia commit, push, pull o merge.
Il core legge soltanto il contenuto consolidato attivo, mai direttamente i file in
corso di modifica. Una modifica salvata ma non consolidata, o un tentativo fallito,
non deve mescolare testo nuovo e vecchi risultati di ricerca. Si conserva l'ultimo
corpus valido; se un errore tecnico non permette di garantirne l'integrità, si
segnala l'indisponibilità invece di usare uno stato parziale. Si riusa il percorso
esistente di preparazione del candidato e attivazione, senza creare un secondo
archivio autorevole o un sistema di coordinamento delle sessioni.
L'archivio mantenuto è una working tree persistente, versionabile nel repository
che contiene quelle Evidence; può riusare il repository del workspace. Draft e
unità curate restano contenuti distinti anche se ospitati nello stesso repository.
Non si modifica una materializzazione temporanea o uno snapshot runtime ricreato
dal preprocessing. Setup e pagina indicano working tree, cartella delle Evidence,
branch e remoto configurati; non si creano automaticamente repository o remoti.
Il seguito manuale, documentato come sequenza da eseguire nel repository corretto, è:
```text
git -C <repository-root> status --short
tht evidence consolidate <workspace-root> # comando previsto, da implementare
git -C <repository-root> add -- <file-evidence-e-metadati-indicati>
git -C <repository-root> commit -m "Update Evidence"
git -C <repository-root> push
```
Il controllo umano usa i normali comandi Git nel terminale. `git status --short`
mostra quali file sono cambiati; `git diff HEAD` permette, quando serve, di vedere
le righe cambiate prima del consolidamento. `git diff --cached` è disponibile per
controllare ciò che si sta per committare. Non sono richiesti un visualizzatore
nell'applicazione, uno strumento grafico, due revisioni obbligatorie o un nuovo gate.
I controlli strutturali del consolidamento vengono invece sempre eseguiti.
Si includono aggiunte, modifiche e cancellazioni dei dati primari necessari alla
ricostruzione, compresi manifest e informazioni di cura manuale quando cambiano.
Indici, cache, sessioni e segreti non fanno parte del commit Evidence. L'operatore
controlla aggiunte, modifiche e cancellazioni prima di consolidare; i file nuovi
segnalati da status vanno letti, perché non compaiono ancora nel diff dei file
tracciati. L'output del consolidamento indica anche i metadati generati da includere
nel commit. Errori Git, credenziali, branch senza upstream e conflitti vengono
risolti con i normali strumenti Git, senza retry o risoluzioni automatiche.
L'ordine ha una conseguenza esplicita: un consolidamento riuscito aggiorna il core
locale prima di commit e push. Se il push manca o fallisce, la modifica rimane locale
e non è ancora trasferita al remoto; l'operatore completa il seguito Git. Un push
riuscito, da solo, non aggiorna altre installazioni. Nessun processo sorveglia i file
o impone che l'operatore abbia completato la sequenza prima di riprendere il lavoro.
## Persistenza e contratto da evolvere
Il proprietario ha approvato la risoluzione persistente dei conflitti fra Memory ed
Evidence nel round Q4 della discussione Memory. Quando la decisione richiede correggere
un'Evidence, il core prepara una proposta che identifica l'unità e mostra il testo
o l'ambito risultante. La correzione passa attraverso l'authoring e la pubblicazione
di questo modulo, con le relative autorizzazioni e validazioni. L'accettazione della
correzione da parte di un utente autorizzato avvia lo stesso salvataggio con attivazione
automatica. L'interfaccia distingue una proposta da approvare, un aggiornamento in
corso o fallito e una correzione già attiva; non basta risolvere soltanto
la domanda corrente. È sempre possibile dichiarare inadeguate le opzioni proposte.
Oggi il [lifecycle Evidence](../evidence.md) attribuisce l'authoring al repository
del workspace: ThothII non lo modifica, non crea commit e non esegue push.
Il [contratto Workspace Evidence v3](../contracts/workspace-evidence-v3.md) lega la
pubblicazione a una revisione coerente del workspace. La manutenzione dei file locali
richiede un'evoluzione esplicita di questo percorso e del formato editabile.
**Q11, riesaminata e accettata per l'archivio locale:** la draft dello specialista deve restare producibile e
consegnabile senza accesso al PostgreSQL o alla stessa installazione. La
scelta aggiornata conserva le fonti in file/repository esterni e le
Evidence raffinate in un archivio locale persistente su file Markdown, gestito dal sistema.
Il CRUD modifica tale archivio e aggiorna l'indice senza commit/push verso le fonti.
L'alternativa di rendere PostgreSQL autorevole anche per Evidence, suggerita nella
prima parte della revisione, è ritirata. PostgreSQL rimane l'archivio delle Memory.
I file locali curati sono dati primari: sopravvivono a Clear e reindicizzazione e
devono essere inclusi nelle copie di sicurezza dell'installazione. Una loro modifica
non aggiorna automaticamente i documenti dello specialista o altre installazioni;
un eventuale trasferimento dei file resta esplicito, senza sincronizzazione bidirezionale.
Il comportamento deve coprire le origini supportate dal prodotto: filesystem/Git,
HTTP e S3. Nella proposta riveduta sono origini di acquisizione delle fonti e dei
contenuti già disponibili. Servono importazione e provenienza coerenti; le unità
raffinate e le modifiche manuali vengono conservate nell'archivio locale.
La gestione delle Evidence non richiede di aggiungere scritture sui server HTTP o S3
di origine. Non si può dichiarare completato il CRUD lasciando queste unità in sola
lettura senza un percorso di importazione utilizzabile.
Il descriptor `evidence.source` indica oggi l'origine acquisita dal preprocessing:
non è già un importatore di documenti nel repository di authoring. Inoltre il
runtime normalizza HTTP/S3 come documenti generici, mentre il parser canonico delle
unità tipizzate viene applicato ai file sotto `curated/<kind>/`. La provenienza
canonica ammette solo un percorso locale sotto `source/`; URI e fingerprint remoti
esistono invece nel corpus acquisito. L'importazione deve collegare questi due
livelli senza fingere che il percorso tipizzato sia già uniforme fra i trasporti.
Modifiche e ritiri devono sopravvivere alla successiva preparazione, sincronizzazione
e reindicizzazione. La rigenerazione da una fonte non deve ripristinare silenziosamente
una unità cancellata o sovrascrivere la correzione del curatore. La semantica di
conflitto fra nuova Source Evidence e cura manuale è approvata in Q13.
Il runtime attuale non garantisce questa protezione durevole: `evidence prepare`
rifiuta modifiche non committate ai file curati e al manifest, ma una fonte cambiata
può rigenerare anche unità corrette manualmente e già committate. Il ritiro esplicito
rimuove l'unità e i riferimenti nel manifest, senza conservare una soppressione che
impedisca a una successiva generazione di riproporla. Questi comportamenti sono
verificati in `harness/tht/evidence/authoring.py`; la gestione amministrativa richiede
di evolverli, non solo di esporli come comandi dell'editor.
**Q13, approvata:** se la fonte aggiornata contraddice una correzione
manuale, conservare in uso l'Evidence salvata dall'amministratore e mostrare il
confronto in Evidence management. Una sostituzione richiede una scelta esplicita,
seguita dalla stessa operazione di attivazione. La conseguenza è che la regola manuale può restare
attiva anche se la fonte più recente dice altro, finché il conflitto non viene
risolto. Il proprietario accetta questa conseguenza. La decisione comprende la
permanenza delle cancellazioni e la protezione da sovrascritture silenziose: una
nuova preparazione non deve riproporre automaticamente conoscenze eliminate.
Validazione e pubblicazione sono passaggi interni dell'unica operazione di salvataggio;
un errore non dichiara attivo il nuovo contenuto. Il sistema continua a rispettare
provenienza e coerenza del contenuto; il salvataggio esplicito dell'amministratore
costituisce l'approvazione della modifica manuale. La decisione sullo storico delle
Memory non elimina questi requisiti del dominio Evidence.
## Creazione manuale e aggiornamento delle fonti — Q14 e Q15 approvate
**Q14, approvata:** consentire di scrivere una
Evidence senza dover fornire un documento esterno. In release 0 la si scrive in un
nuovo file Markdown nell'archivio indicato, usando l'esempio documentato.
Il sistema registra una dichiarazione manuale come fonte gestita e la distingue
dall'informazione derivata da documenti. Lo stesso criterio vale per una correzione
che cambia il significato del contenuto: la dichiarazione dell'amministratore
sostiene il testo corrente, mentre il documento originario resta collegato come
origine e per rilevarne gli aggiornamenti. Non deve essere mostrato come prova di
una regola che non contiene. Il consolidamento acquisisce anche le nuove unità manuali.
Oggi la provenienza canonica richiede fonte locale, hash ed estratti: non esiste
un'origine manuale esplicita. La validazione verifica integrità e presenza testuale
degli estratti, ma non dimostra la verità o il supporto semantico della regola.
Attuare Q14 richiede distinguere nel contratto la fonte del testo corrente dal
documento originario; non richiede inventare estratti o una validazione semantica
automatica presentata come garanzia di correttezza.
**Q15, approvata:** riacquisire le fonti esterne su richiesta
esplicita dell'amministratore, senza controlli periodici o accessi alle fonti a ogni
domanda o salvataggio di una card. Fra due aggiornamenti richiesti, il sistema usa
quanto già acquisito. Il consolidamento continua ad attivare la modifica locale
senza richiedere prima un aggiornamento della fonte remota; eventuali conflitti
scoperti alla successiva acquisizione seguono Q13.
Il proprietario ha approvato entrambe le raccomandazioni il 2026-09-08. Il successivo
riesame conserva questi comportamenti e rende esplicita l'autonomia dello specialista
che produce le draft. I dettagli tecnici seguenti descrivono la proposta semplificata.
## Piano esecutivo
### E1 — Raffinamento e archivio canonico locale
**Modifica del sottosistema Evidence core.** Rendere editabile il Markdown cambia
il contratto che il codice attuale legge e scrive. Questa attività precede la
pagina amministrativa: non è una modifica limitata alla presentazione o all'editor.
Il nuovo formato conserva contenuti tipizzati, ambito e provenienza richiesti dal
core, ma usa il testo visibile come unica rappresentazione autorevole del contenuto
umano. Va identificato come una nuova versione del contratto Curated unit, distinta
dalla versione del descriptor del workspace.
L'intervento coordinato comprende:
- parser, renderer e validazione in `harness/tht/evidence/canonical.py`;
- preparazione, scritture e migrazione in `harness/tht/evidence/authoring.py`,
con adeguamento delle istruzioni e degli esempi usati nel raffinamento;
- acquisizione e normalizzazione in `harness/tht/evidence/corpus/normalize.py`,
consolidamento e preprocessing, affinché producano il contenuto tipizzato atteso
dall'indicizzazione e dal recall;
- correzioni deliberate dal core, provenienza e citazioni, che devono consumare
e aggiornare coerentemente la nuova rappresentazione;
- contratto documentato e test del percorso completo: modifica del testo visibile,
consolidamento, contenuto indicizzato e successivo utilizzo da parte del core.
Le Evidence esistenti devono essere convertite esplicitamente al nuovo formato
e reindicizzate, verificando il mantenimento del contenuto e degli identificatori.
La fase di sviluppo permette un passaggio unico; non è richiesto mantenere due
formati di authoring concorrenti. Una conversione non rappresentabile fedelmente
va segnalata. Si preserva il modello tipizzato interno dove possibile, per contenere
la modifica nei componenti che dipendono dal documento senza ridisegnare le fasi
del workflow NL→SQL.
Estendere i contratti in `harness/tht/evidence/` per distinguere fonte corrente,
dichiarazione manuale e documento originario. La dichiarazione viene gestita
dall'applicazione insieme all'unità locale; chi scrive il Markdown non deve
creare un file sorgente separato o gestire hash ed estratti. I campi per kind
mantengono la validazione deterministica, senza attribuirle una verifica della
verità della regola. Aggiornare il glossario e il contratto canonico con il codice.
Riutilizzare raffinamento, parser, renderer e validazione esistenti, distinguendo
la working tree locale persistente dagli snapshot acquisiti e adattando il formato al testo Markdown
editabile come fonte autorevole, senza duplicazione nascosta del contenuto umano.
Documentare posizione dei file, campi richiesti e un esempio per la creazione.
L'applicazione acquisisce le draft e
conserva il risultato in file locali persistenti. Le mutazioni aggiornano contenuto
canonico, presentazione Markdown e manifest; le scritture sono serializzate per
workspace e controllano la versione corrente del record; una modifica concorrente
non viene risolta sovrascrivendo silenziosamente il contenuto altrui.
Il salvataggio non richiede commit, push o un database condiviso con lo specialista.
L'archivio locale è distinto dalla materializzazione runtime ricostruita da Git e
dal corpus derivato. Consolidamento e preprocessing acquisiscono i contenuti locali
curati; il core consulta soltanto il corpus attivo validato. Clear preserva i file
e la ricostruzione dell'indice riparte da essi, senza sovrascriverli
con nuove copie delle draft originali.
La persistenza conserva la cura manuale e le esclusioni necessarie a impedire la
ricomparsa delle unità eliminate. La rigenerazione confronta le proposte con tale
stato: assegnare un nuovo ID a un contenuto derivato non deve essere un modo per
riattivarlo automaticamente aggirando una cancellazione. Le nuove proposte e i
conflitti restano distinti dal corpus già approvato e attivo.
Verificare con archivi locali temporanei draft esterne e raffinamento, creazione
manuale, cambiamento di significato
con provenienza corretta, round trip del formato canonico, aggiornamento del manifest,
cancellazione, conflitto fra scritture e retry. La validazione di un estratto
presente nella fonte non viene usata come prova automatica di supporto semantico.
### E2 — Pagina amministrativa e consolidamento manuale
Esporre API amministrative per elenco completo, filtri, dettaglio, mutazioni ed
esito delle operazioni. Il backend applica autorizzazioni e isolamento, poi chiama
il servizio di authoring del harness. Realizzare la pagina autonoma nell'AppShell
con provenienza leggibile, percorsi effettivi dei file sull'host e istruzioni per
modificarli con editor esterni, consolidarli e completare commit/push manualmente.
Il comando di consolidamento acquisisce aggiunte, modifiche e rimozioni locali
tramite lo stesso servizio. Nessun editor, form di contenuto o
trasferimento file web è richiesto in R0; non serve una sessione del core.
Evolvere lo stage interno `runEvidenceStage` e il percorso harness
`tht preprocess evidence` per attivare il contenuto canonico locale autorizzato.
Il salvataggio non avvia una riacquisizione HTTP/S3, una nuova estrazione dalle
Source Evidence o una scansione DWH. Il nuovo comando manuale Evidence riusa questo
stage interno senza richiedere un preprocessing completo per ogni modifica.
Preparare e verificare il candidato Evidence prima di attivarlo. L'indicizzazione
e la rimozione devono essere circoscritte al workspace e alle generazioni Evidence
interessate nella collezione Reference condivisa. Preservare Schema, relazioni,
LSH e Memory: il salvataggio non usa Preprocessing Clear e non ricrea l'intera
collezione. Se l'attivazione fallisce, mostrare l'esito parziale e rendere ripetibile
il completamento della stessa modifica, senza chiedere una nuova approvazione.
Adeguare la ricerca perché consumi l'archivio locale aggiornato, senza richiedere
commit della fonte o cambiare configurazioni estranee. Adeguare il calcolo della
readiness: aggiornare Evidence non dichiara risolto un blocco
di Schema o Catalog. Dopo un Clear, l'eventuale necessità di preprocessing completo
rimane esplicita; il solo consolidamento non ricostruisce tutti i derivati mancanti.
Verificare che i percorsi mostrati portino ai file effettivi anche su installazioni
Docker e che una modifica alla frase visibile sia acquisita senza editing di metadati
tecnici. Verificare creazione/modifica/cancellazione con esito disponibile al core, errore
di indicizzazione dopo persistenza, retry e isolamento delle altre componenti
della collezione. Le successive elaborazioni devono usare il contenuto corrente
senza recuperare unità cancellate. Verificare le correzioni deliberate dalla sessione
stessa, senza aggiungere prove di CRUD amministrativo concorrente al core. Un
problema preesistente di Catalog deve continuare a impedire una falsa readiness.
Verificare che errori strutturali impediscano l'attivazione e producano indicazioni
correggibili, che il comando sia rieseguibile e che non esegua operazioni Git.
Verificare che file modificati senza consolidamento e tentativi falliti non cambino
il contenuto usato dal core né combinino versioni differenti di testo e indice.
Provare la sequenza documentata di commit/push con repository temporanei e remoto
locale, verificando l'inclusione delle cancellazioni e dei metadati necessari.
### E3 — Importazione, refresh esplicito e conflitti con le fonti
Fornire un percorso di importazione locale per le origini già supportate,
conservando contenuto acquisito e provenienza remota. Uniformare il passaggio alle
unità canoniche: i documenti HTTP/S3 oggi normalizzati genericamente devono diventare
file locali editabili. Le unità importate restano gestibili senza credenziali di
scrittura sui server fonte.
L'azione amministrativa di aggiornamento delle fonti riacquisisce il contenuto e
confronta le impronte con quanto già acquisito. Per le unità curate interessate,
prepara il confronto con le proposte risultanti senza sostituire la dichiarazione
manuale attiva. Questa protezione non dipende dalla capacità del modello di
riconoscere ogni contraddizione semantica. La scelta dell'amministratore usa poi
lo stesso salvataggio con attivazione; fonti invariate non richiedono nuova cura.
Verificare con origini controllate che la riacquisizione avvenga soltanto su richiesta,
che un errore di accesso non venga scambiato per una cancellazione e che consolidamento/recall
non chiamino i connettori di acquisizione remota. Verificare aggiornamento di una
fonte collegata a una correzione manuale, permanenza della versione curata, decisione
di sostituzione e mancata ricomparsa automatica delle unità eliminate.
La lettura può essere consegnata come incremento intermedio di E2, ma il progetto
è completo soltanto con CRUD, attivazione e gestione delle origini previste. X1 nel
piano comune collega le proposte provenienti dai conflitti con Memory e verifica
che raggiungano questi stessi servizi con autorizzazioni ed esiti coerenti.
## Criteri di completamento
- Tutte le unità sono raggiungibili mediante elenco e filtri, senza una sessione.
- La pagina mostra cartella e percorso effettivo sull'host per ogni unità. Markdown,
campi richiesti ed esempi permettono creazione e modifica con editor esterni;
i dati invalidi non vengono attivati e gli errori indicano il file da correggere.
- Una draft prodotta fuori dall'installazione può essere acquisita, raffinata e
conservata localmente senza accesso dello specialista a PostgreSQL.
- Creazione, modifica e ritiro aggiornano l'archivio locale e il corpus usato dal core;
non richiedono commit/push nel repository delle fonti.
- Una Evidence può essere creata senza documento esterno; la dichiarazione manuale
è riconoscibile e il documento originario di una correzione non è mostrato come
supporto di un'affermazione che non contiene.
- Una modifica o cancellazione completata resta valida dopo una nuova preparazione
e reindicizzazione; non rimangono frammenti ricercabili della versione rimossa.
- Una fonte aggiornata in conflitto con la cura manuale non sostituisce l'Evidence
attiva: il confronto resta disponibile all'amministratore fino alla sua decisione.
- Il consolidamento manuale completa anche l'attivazione, senza un successivo `Publish`; l'esito
distingue operazione in corso, completata ed eventuali errori con retry.
- Il core usa soltanto l'ultimo corpus consolidato valido. File in lavorazione o
consolidamenti falliti non diventano disponibili attraverso letture dirette.
- Dopo una modifica completata, le successive elaborazioni recuperano il contenuto
corrente. Dopo una cancellazione completata non recuperano l'unità rimossa.
Nessuna delle due operazioni riscrive decisioni, artefatti o SQL già prodotti.
- Clear e ricostruzione degli indici preservano le Evidence canoniche locali,
incluse le correzioni e le esclusioni dovute a cancellazioni.
- Le correzioni originate da conflitti con Memory raggiungono lo stesso percorso
autorevole delle modifiche amministrative e mostrano il proprio stato di pubblicazione.
- Le origini senza scrittura dispongono di un percorso utilizzabile verso l'authoring.
- Le fonti esterne vengono riacquisite solo su richiesta dell'amministratore;
il consolidamento e il recall usano il contenuto locale già acquisito.
- API e pagina applicano accesso amministrativo e isolamento dei workspace.
- Le istruzioni identificano working tree e file effettivi; il seguito manuale
include verifica del diff, commit e push. Nessun watcher o comando Git automatico
è introdotto. L'esito distingue disponibilità locale e trasferimento al remoto.
- Il salvataggio delle Evidence preserva Memory, Schema, relazioni, LSH e Catalog
Metadata; non elimina blocchi di readiness estranei all'aggiornamento Evidence.
@@ -0,0 +1,261 @@
# Amministrazione di Memory ed Evidence
Data del piano: 2026-09-08. Aggiornamento 2026-09-09: M1–M3, E1–E3 e X1
implementati. Risultati e limiti della verifica finale sono raccolti nel
[rapporto X1](2026-09-09-archive-repair-x1-validation.md).
La [revisione di semplicità](2026-09-08-memory-evidence-simplification-review.md)
riesamina tutte le decisioni Q1–Q15 alla luce degli ultimi chiarimenti del
proprietario e confronta le conseguenze con il piano precedente. Per Evidence R0
sono scelti file Markdown locali, editor esterni e consolidamento manuale con
controllo della struttura, seguito da diff, commit e push dell'operatore. La pagina
deve indicare chiaramente percorsi e comandi. Le restanti semplificazioni sono
confermate. Il cambio del formato richiede adeguare il sottosistema core Evidence,
convertire i file e reindicizzare; E1 precede la pagina. Il controllo umano usa
lo stato Git nel terminale, con diff delle righe quando serve, senza una UI dedicata.
Si ignora la contemporaneità fra amministrazione e core. Lo
specialista scrive draft indipendentemente dall'installazione; il sistema le
raffina e le conserva localmente. La proposta aggiornata usa file canonici locali
per le Evidence e toglie commit/push automatici dal CRUD; questa revisione tecnica di Q11
sostituisce la raccomandazione precedente ed è stata attuata negli incrementi E1–E3.
## Obiettivo e decisioni del proprietario
L'amministratore deve poter accedere in qualsiasi momento alle Memory e alle Evidence
registrate, cercarle, filtrarle, aprirle, crearle, modificarle e cancellarle. L'accesso
non richiede una sessione del core o una fase del workflow.
Il lavoro è diviso in due progetti autonomi:
- [Memory management](2026-09-08-memory-management.md);
- [Evidence management](2026-09-08-evidence-management.md).
I progetti condividono l'esperienza di gestione e i componenti appropriati, mantenendo
distinti i contenuti, le regole di validazione e i percorsi di persistenza.
### Collocazione dei due accessi
L'ordine previsto nell'accordion Administration è:
```text
Administration
Database management
Memory management
Evidence management
────────────────────
Workspace management
Pi management
```
Memory management ed Evidence management sono due voci autonome, allo stesso livello
di Database management. «Sotto» indica soltanto la posizione fisica nella navigazione.
Non sono sottopagine, tab o funzionalità di Database management. Ciascuna apre la
propria pagina e possiede il proprio stato di navigazione.
### Decisioni già acquisite sulla Memory
- La Memory serve a migliorare schema linking e generazione SQL di domande future.
- Il perimetro comprende chiarimenti di dominio riutilizzabili, regole corrette per
join, filtri e aggregazioni, domande risolte consultabili ed errori da evitare
quando il motivo è stato compreso e approvato. Le scelte occasionali non diventano
regole generali; possono restare nel contesto di un exemplar.
- Il formato della memory card è allineato per analogia a quello delle Evidence;
origine e dominio restano separati. Le Memory non entrano nel canone Evidence.
- La gestione avviene tramite CRUD e form interni a ThothII.
- L'evoluzione del modulo Memory è interna a ThothII, riusando l'infrastruttura
dell'installazione e senza adottare un framework esterno per governarne il comportamento.
- Il progetto comprende ricerca semantica e lessicale ibrida in Qdrant, filtri
sull'ambito e collegamenti espliciti fra card percorsi dal core. Non introduce
un database a grafi dedicato e non rinvia il grafo a una successiva sperimentazione.
- PostgreSQL, già presente nell'installazione, è l'archivio autorevole di card,
collegamenti e dipendenze, in tabelle proprie del modulo Memory. Sostituisce il
registro JSONL; Qdrant è una proiezione rigenerabile, con sincronizzazione esplicita.
- I collegamenti sono proposti e approvati insieme alle card nel riepilogo finale
e gestibili manualmente da Administration. Cancellare una card elimina anche i
collegamenti che la coinvolgono, conservando le altre card.
- Gli exemplar `solved_question` usano il formato card con consumo consultativo.
- Il core prepara durante il lavoro un riepilogo finale modificabile delle nuove
Memory e degli aggiornamenti proposti. Il reviewer seleziona cosa salvare; la
cancellazione resta nel CRUD amministrativo.
- Le Memory vengono proposte nei gate pertinenti: chiarimenti all'inizio, regole di
collegamento nello schema linking e regole di calcolo durante la costruzione SQL.
Le approvazioni sono integrate nei gate, senza una domanda separata per ogni card.
- Una modifica sostituisce il contenuto corrente: non è richiesta una cronologia
aggiuntiva delle revisioni delle Memory o una ricostruibilità storica dedicata.
- Se un riferimento allo schema rende una Memory inutilizzabile, il proprietario
sceglie la cancellazione anziché lo stato «Needs review». Dopo una sincronizzazione
riuscita dello schema fisico, il backend comunica gli elementi rimossi al modulo
Memory, che cancella le card dipendenti e le proiezioni. La relazione fra card ed
elementi dello schema deve essere strutturata; cleanup del Catalog ed errori di
connessione non sono prove di rimozione fisica.
- I conflitti fra Memory ed Evidence si risolvono con azioni chiuse, specifiche e
accompagnate dal contenuto risultante. La scelta alimenta una correzione degli
archivi attraverso i rispettivi percorsi, con stato di pubblicazione esplicito;
è sempre possibile dichiarare inadeguate le proposte e richiederne la riformulazione.
- La verifica usa test funzionali automatici e regressioni per casi concreti; non
promette un miglioramento qualitativo generale o un benchmark con/senza Memory.
- Le sessioni esistenti non vincolano il design. Il loro azzeramento durante lo
sviluppo, se necessario, è autorizzato; non è un'operazione eseguita da questi documenti.
La decisione di non conservare uno storico riguarda le Memory. Non modifica
automaticamente il contratto di authoring e pubblicazione delle Evidence.
### Salvataggio delle Evidence
In Q12 il proprietario rifiuta la separazione fra `Save draft` e `Publish`.
La scelta successiva dell'editor esterno sostituisce il Save della form con il
salvataggio dei file e un comando manuale di consolidamento: verifica la struttura,
indica le correzioni necessarie e, se valido, aggiorna metadati, corpus e indice.
Acquisisce anche aggiunte e cancellazioni. Segue il controllo del diff con commit e
push manuali dell'operatore; non sono previsti watcher, Git automatico o un editor
in Evidence management. La pagina mostra cartella, percorsi sull'host e comandi;
il [piano Evidence](2026-09-08-evidence-management.md) specifica la sequenza.
Il consolidamento attiva localmente il contenuto; un push mancante o fallito lascia
il trasferimento al remoto da completare manualmente. L'ultimo chiarimento
elimina il requisito di aggiornare sessioni aperte a seguito di modifiche
amministrative: durante tali modifiche il core è fermo o la contemporaneità può
essere ignorata. Rimangono le correzioni deliberate dalla sessione stessa.
La review di Q11 distingue le draft dello specialista dalle Evidence raffinate
locali; raccomanda di preservare le prime e gestire le seconde su file persistenti,
senza PostgreSQL condiviso o scritture automatiche nel repository delle fonti.
In Q13 è approvata
la precedenza della correzione manuale quando una fonte aggiornata la contraddice:
resta attiva fino alla risoluzione esplicita del confronto in Evidence management.
Le Evidence cancellate non ricompaiono automaticamente durante la rigenerazione.
Q14 consente di crearle senza documento esterno, in R0 tramite un nuovo Markdown: una
dichiarazione manuale sostiene il testo corrente, con l'eventuale documento
originario conservato come provenienza distinta. Q15 limita la riacquisizione
delle fonti esterne a una richiesta esplicita dell'amministratore; il normale
salvataggio e il recall usano il contenuto già acquisito.
L'[ADR 0019](../adr/0019-author-evidence-in-app-with-automatic-activation.md) registra
l'evoluzione del contratto, ancora da implementare.
## Stato verificato nel repository
`frontend/src/shell/AppShell.tsx` contiene Administration con Database management,
Workspace management e Pi management. Non contiene le due pagine richieste.
Il modulo Memory espone già comandi CLI per elenco, dettaglio, aggiornamento,
cancellazione e ricerca, ma manca una superficie CRUD amministrativa web.
Il riepilogo di una sessione mostra soltanto le Memory collegate alla sessione.
Il [contratto attuale della Memory](../gestione-memory.md) descrive registro JSONL
canonico e indice Qdrant derivato.
Le Evidence hanno già un percorso di consultazione e modifica dei documenti nel
repository di authoring, descritto in [Evidence: sources, preparation, and review](../evidence.md).
Manca una pagina amministrativa per queste operazioni dentro ThothII. Il contratto
attuale prevede che ThothII legga e pubblichi il repository, senza modificarlo,
creare commit o eseguire push: l'editing amministrativo richiede evolvere questo
confine, come esplicitato nel progetto Evidence.
## Esperienza comune
L'amministratore lavora nell'interfaccia operativa esistente, con una lista densa
e leggibile e un'area di dettaglio. Si riusano tema, controlli, focus e navigazione
già definiti in PRODUCT.md e DESIGN.md.
- Selettore di workspace, ricerca testuale, filtri combinabili, ordinamento e paginazione.
- Elenco completo dei record persistiti, indipendente dalla disponibilità della
ricerca semantica. Una ricerca per similarità può affiancarlo, senza limitarlo ai
pochi risultati del recall del core.
- Apertura del contenuto completo, dell'ambito di applicazione e della provenienza.
- Creazione e modifica Memory mediante form; per Evidence R0, percorsi ed esempi
Markdown per editor esterni, consolidamento e seguito Git manuali.
- Cancellazione con indicazione precisa dell'oggetto e del suo effetto; per Evidence
R0 la rimozione del file è acquisita dal consolidamento.
- Stato esplicito di salvataggio e disponibilità per il core, con recupero dagli errori.
- Filtri e posizione nell'elenco conservati quando si apre e si chiude un record.
- Controlli utilizzabili da tastiera; stato vuoto, nessun risultato e indisponibilità
del servizio distinguibili. Chrome in inglese, contenuti nella lingua del workspace.
Le due pagine non dipendono dalla selezione di un database in Database management.
Eventuali filtri su tabelle e colonne usano riferimenti al catalogo quando disponibili;
la loro assenza non impedisce di consultare i contenuti registrati.
Componenti condivisibili: barra di ricerca e filtri, lista, paginazione, struttura
del dettaglio, campi comuni della card, provenienza e feedback delle operazioni.
Form Memory, istruzioni di manutenzione Evidence, autorizzazione, validazione, pubblicazione e
persistenza rimangono responsabilità dei rispettivi moduli. Il riuso del frontend
non introduce un archivio canonico unico per Memory ed Evidence.
## Confini e integrazione
Entrambe le pagine appartengono ad Administration e devono applicare il controllo
amministrativo anche nelle API. La collocazione visiva non assegna automaticamente
le autorizzazioni di Database management ai nuovi moduli.
Il progetto Memory può essere consegnato senza attendere il progetto Evidence.
I componenti comuni si estraggono quando servono ai flussi reali di entrambi.
La verifica finale congiunta copre ordine della navigazione, accesso indipendente,
filtri, percorsi di manutenzione e feedback coerenti, isolamento fra workspace e assenza di effetti
incrociati fra i due domini.
La separazione fra Reference Vector Collection e Memory Vector Collection rimane
quella dell'[ADR 0017](../adr/0017-separate-reference-vectors-from-runtime-memory.md).
Un'operazione amministrativa sulle Evidence non cancella le Memory; una cancellazione
di Memory non elimina Evidence, Schema o metadati del database.
## Piano esecutivo dei due progetti
Il riesame mantiene i requisiti funzionali Q1–Q15 e semplifica le scelte tecniche
secondo le condizioni descritte sopra. L'ordine di lavoro parte dalla Memory e
riusa poi i componenti effettivamente comuni per Evidence management. Le API e
il coordinamento delle scritture attuano questi vincoli; la distinzione fra draft
esterne e archivio locale sostituisce l'ipotesi di un unico archivio Git da modificare.
| Ordine | Incremento | Risultato verificabile | Dipendenze |
| --- | --- | --- | --- |
| 1 | M1 — Archivio e CRUD Memory | PostgreSQL autorevole, API e pagina con elenco completo, filtri, form e cancellazione; collegamenti e dipendenze persistiti, proiezioni aggiornate o invalidate | Nessuna dipendenza dal progetto Evidence |
| 2 | M2 — Ricerca Memory | Ricerca ibrida con filtri ed espansione limitata dei collegamenti; risultati coerenti con il contenuto corrente | M1 |
| 3 | M3 — Memory nel workflow | Riepilogo finale, uso delle categorie nei gate e cancellazione dopo sincronizzazione fisica riuscita | M1 e M2 |
| 4 | E1 — Preparazione e archivio Evidence | Draft esterne, raffinamento del sistema, file canonici locali persistenti e protezione delle correzioni/cancellazioni | Q11–Q15 riesaminate; non richiede il runtime Memory |
| 5 | E2 — Manutenzione e consolidamento Evidence | Pagina con percorsi dei Markdown; comando manuale di verifica e attivazione; istruzioni per diff, commit e push | E1 |
| 6 | E3 — Fonti e conflitti di aggiornamento | Importazione delle origini supportate, refresh esplicito, confronto con le correzioni manuali | E1 ed E2 |
| 7 | X1 — Integrazione finale | Correzioni persistenti dei conflitti Memory/Evidence e verifica congiunta delle due pagine | M3 ed E3 |
E1–E3 sono un progetto separato: la sequenza è l'ordine operativo scelto per questa
consegna, non una dipendenza tecnica dal modulo Memory. Il CRUD Memory è utilizzabile
come primo incremento; M2 e M3 restano obbligatori nel progetto attuale. La correzione
completa dei conflitti fra i due archivi si considera consegnata soltanto con X1.
### Responsabilità di implementazione
- Il harness possiede contratti, persistenza e operazioni dei moduli Memory ed
Evidence. Il backend applica autorizzazioni, espone le API e orchestra le
operazioni; non introduce una seconda implementazione delle stesse scritture.
- Il Metadata Catalog rimane responsabilità del backend. Una sincronizzazione
fisica riuscita chiama la pulizia Memory nello stesso flusso, con gli elementi
effettivamente rimossi nell'ambito controllato e un recupero dopo interruzione.
La pulizia non dipende dalla UI e non richiede un bus di eventi.
- Le pagine condividono controlli di consultazione e feedback; logica dei kind, fonte autorevole e
attivazione rimangono nei rispettivi moduli. L'AppShell ospita le due voci autonome
nell'ordine concordato.
- Le modifiche coordinate hanno gestione esplicita di errori e retry. I controlli
sulla versione corrente impediscono sovrascritture inconsapevoli senza richiedere
uno storico delle Memory. I retry non duplicano card o collegamenti. Il normale
salvataggio è sequenziale, con un esito persistente da recuperare se incompleto;
non richiede nuovi worker, code generiche o coordinamento delle sessioni aperte.
### Verifiche e chiusura della consegna
Ogni incremento esegue i test delle operazioni che cambia e i controlli dei layer
coinvolti: pytest/ruff per il harness, vitest e typecheck per backend/frontend.
Le pagine sono verificate anche nel browser per navigazione, filtri, form, errori
e uso da tastiera. I contratti e i test specifici sono nei due piani di progetto.
X1 verifica sia i conflitti che correggono Memory sia quelli che correggono Evidence,
inclusi utente privo dell'autorizzazione necessaria, proposta rifiutata, errore di
attivazione e retry. Il gate deve mostrare lo stato reale dell'archivio: la sola
risoluzione della domanda corrente non dimostra che la correzione sia persistita.
La verifica congiunta copre inoltre isolamento dei workspace, ordine dei link e
assenza di cancellazioni incrociate.
I contratti correnti vengono aggiornati insieme al relativo codice. Alla fine si
aggiornano PROJECT_STATE.md e documentazione operativa e si esegue la build strict.
La verifica della generazione con un modello reale rimane distinta dai test
deterministici; non si promette un benchmark generale di miglioramento qualitativo.
Gli incrementi approvati sono implementati e disponibili nel Docker locale.
I rapporti di validazione distinguono i test deterministici, i servizi reali e le
prove con il modello configurato. Le verifiche sintetiche non modificano la
conoscenza PSD; la valutazione dei contenuti reali rimane una decisione del reviewer.
@@ -0,0 +1,253 @@
# Revisione di semplicità: Memory ed Evidence
Data: 2026-09-08. Stato: archivio locale Markdown, editor esterni e consolidamento
manuale con seguito Git scelti per la release 0; conseguenze esplicitate,
restanti semplificazioni confermate, chiariti impatto core e controllo Git.
Nessuna modifica applicativa.
## Condizioni che guidano la revisione
Chi amministra il sistema è competente e deve vedere chiaramente cosa produce ogni
azione. Durante il CRUD amministrativo di Memory ed Evidence si può assumere che
non ci siano attività core in corso; la loro eventuale contemporaneità non è un
caso da supportare con meccanismi dedicati.
Lo specialista di contesto può essere una persona diversa da chi gestisce
l'installazione. Scrive le draft delle Evidence senza dover accedere a PostgreSQL,
amministrarlo o disporre della stessa installazione. Il flusso fondamentale resta:
1. Lo specialista scrive e consegna le draft in documenti accessibili al sistema.
2. Il sistema le acquisisce e le raffina in Evidence strutturate.
3. Il sistema conserva localmente le Evidence per consultazione e manutenzione.
La proposta avanzata durante la revisione di usare PostgreSQL come archivio
autorevole anche delle Evidence è ritirata. Confrontava il costo del solo CRUD
interno, senza rappresentare adeguatamente l'autonomia di chi produce le fonti.
Un database dietro un'interfaccia non richiederebbe di per sé accesso SQL agli autori,
né un database condiviso; questo però non risolve da solo il flusso di redazione e
consegna esterno. Non propongo di introdurre tale dipendenza.
## Archivio locale delle Evidence accettato dopo il chiarimento
Il proprietario ha accettato i file locali e richiede una gestione facile da trovare
e usare anche per uno specialista non tecnico. Il formato di lavoro è Markdown;
JSONL non è una superficie di gestione delle Evidence. Il proprietario ha scelto
editor esterni per la release 0, chiedendo di indicare chiaramente dove sono i file.
| Contenuto | Responsabile | Conservazione e uso |
| --- | --- | --- |
| Draft originali | Specialista di contesto | File o repository delle fonti, redigibili e consegnabili indipendentemente dall'installazione |
| Evidence raffinate e correzioni locali | Sistema e persone autorizzate alla manutenzione del contenuto | File Markdown in un archivio locale persistente dell'installazione, consultabili dall'applicazione e modificabili con editor esterni |
| Indice di ricerca | Sistema | Qdrant, ricostruibile dalle Evidence locali correnti |
Questa è una revisione della parte tecnica di Q11: distingue l'autorità delle draft
esterne dall'archivio delle Evidence raffinate usate dalla singola installazione.
Il repository delle fonti rimane utilizzabile; il consolidamento delle Evidence
locali non esegue commit o push e non richiede un PostgreSQL condiviso. Il seguito
manuale ora richiesto comprende controllo del diff, commit e push nel repository
che contiene i file curati. L'archivio è una working tree persistente; draft e unità
curate possono stare nello stesso repository, mantenendo distinta la loro funzione.
Le conseguenze devono essere esplicite. Una correzione locale cambia ciò che usa
quell'installazione e non viene rispedita automaticamente allo specialista o ad
altre installazioni. I file restano trasferibili per un passaggio esplicito; non
si costruisce una sincronizzazione bidirezionale. L'archivio locale va conservato
e incluso nelle copie di sicurezza: dopo una correzione non è più un semplice
output eliminabile e rigenerabile dalle draft senza perdita di lavoro.
Memory mantiene PostgreSQL locale come archivio già concordato. Le due pagine
restano separate e riusano controlli comuni; la scelta della persistenza segue il
flusso di produzione dei rispettivi contenuti.
## Gestione della release 0: editor esterni scelti
Il proprietario sceglie il proprio editor sul Mac o PC, oppure vim, nano o
equivalenti sul server. La proposta di editor applicativo è ritirata; non si
integra una libreria di editing né si sviluppano form di contenuto Evidence.
`Evidence management` conserva lista, ricerca, filtri e dettaglio. Mostra la cartella
del workspace e il percorso assoluto di ogni file, copiabile e risolto dalla
configurazione effettiva. Indica l'host su cui si trova; con Docker mostra il percorso
persistente accessibile sull'host. Le istruzioni distinguono draft originali e
Evidence raffinate da manutenere e includono un esempio Markdown per crearne una.
Si modificano direttamente i file nell'archivio locale dell'installazione. Chi
lavora su una copia sul proprio computer la riporta lì con i propri strumenti.
Non sono richiesti upload/download web, nuove cartelle condivise o sincronizzazione.
Dopo aver salvato, aggiunto o rimosso file, l'operatore controlla lo stato Git,
poi esegue un comando manuale di
consolidamento: controlla la struttura attesa, segnala file e correzioni necessarie
e, solo se i controlli passano, aggiorna metadati, corpus e indice. Il comando è
rieseguibile dopo correzioni o errori tecnici. Seguono commit e
push manuali, con le istruzioni della pagina e della documentazione operativa.
Il core consulta solo l'ultimo corpus consolidato valido; i file in lavorazione
e gli aggiornamenti falliti non devono introdurre contenuto parziale nella ricerca.
La richiesta più recente sostituisce la proposta intermedia del pulsante
`Apply file changes`; non servono un'esecuzione dalla UI, watcher o operazioni Git
automatiche. Il salvataggio nell'editor da solo non aggiorna il recall. Il controllo
dei file usa `git status --short` nel terminale; il normale `git diff` è disponibile
per approfondire le righe cambiate, senza visualizzatore web o doppia revisione
obbligatoria. Il comando riporta un breve riepilogo testuale delle modifiche.
Il Markdown locale deve essere realmente editabile: il testo visibile è autorevole,
i campi richiesti sono documentati e i metadati derivati sono gestiti dal sistema.
Il formato v3 attuale, che verifica il rendering contro una copia codificata del
testo, va quindi adattato. Non basta indicare i percorsi dei file attuali e non si
introduce un secondo archivio di scambio da sincronizzare. Questo comporta una
modifica effettiva dei componenti core Evidence: parser, renderer, preparazione,
validazione, normalizzazione e collegamento a indicizzazione/recall. E1 comprende
nuova versione del contratto, conversione dei file esistenti e verifica del percorso
completo; il formato interno tipizzato viene conservato dove possibile.
Creazione, modifica e cancellazione delle unità raffinate passano dai file e dallo
stesso consolidamento. Un errore di accesso all'archivio non è una prova di
cancellazione. Le correzioni approvate dal core continuano a chiamare direttamente
il servizio di scrittura. Il proprietario ha confermato le restanti semplificazioni
chiedendo di esplicitare impatto core e semplicità del controllo Git.
Il [piano Evidence](2026-09-08-evidence-management.md#consolidamento-manuale-e-seguito-git--release-0)
specifica controlli, comando previsto e sequenza Git. Dopo il consolidamento il
contenuto è disponibile localmente; finché commit/push non sono completati, non è
versionato/trasferito al remoto. Il sistema si affida alla disciplina dell'operatore
e non tenta di completare o riparare automaticamente la sequenza Git.
## Conseguenze rispetto al piano precedente alla revisione
Il confronto riguarda il piano concordato prima del riesame: form strutturate per
le unità, repository Git autorevole con scritture applicative e supporto alle
modifiche amministrative durante sessioni aperte. Le funzionalità non erano ancora
implementate: si confrontano due progetti, non una regressione già introdotta.
| Aspetto | Cosa cambia | Conseguenza pratica |
| --- | --- | --- |
| Gestione R0 delle Evidence | Editor esterno scelto al posto di editor e form nell'applicazione | Si usano strumenti già disponibili. La pagina indica i file; servono un Markdown realmente editabile, esempi e validazione in acquisizione. Si rinuncia alla guida e ai controlli durante la digitazione. |
| Applicazione delle modifiche esterne | Salvare i file nell'archivio ed eseguire il consolidamento manuale | Una copia sul Mac o PC va riportata nell'archivio con gli strumenti dell'operatore. Fino al consolidamento riuscito la modifica non è disponibile al core. Gli errori strutturali indicano il seguito necessario. Non si sviluppano trasferimenti file web o watcher. |
| Autorità delle Evidence raffinate | Archivio locale, distinto dalle draft esterne | Una correzione agisce su quell'installazione. Lo specialista che lavora alle draft e altre installazioni non la ricevono automaticamente. |
| Cronologia e distribuzione Git | Controllo del diff, commit e push manuali dopo il consolidamento | Git conserva e trasferisce quanto l'operatore committa e pubblica. La sequenza non è imposta né completata dal sistema: se il push manca o fallisce, il core locale può già usare modifiche non trasferite. Non c'è rollback applicativo o gestione automatica dei conflitti Git. |
| Ripristino dei dati | Le Evidence locali curate sono dati primari | Il backup deve comprenderle. Ricostruire tutto dalle sole draft recupererebbe la base, ma potrebbe perdere correzioni e cancellazioni locali; Clear deve preservare l'archivio. |
| Attività core contemporanee | Non vengono più gestite le modifiche amministrative durante il lavoro core | Se avvengono comunque, non è garantita la coerenza della sessione in corso. Non si aggiornano contesti o SQL già prodotti. Le successive elaborazioni usano il contenuto aggiornato dopo il completamento dell'operazione. |
| Salvataggio e indice | Operazione sequenziale con recupero minimo persistente | L'utente attende l'esito dell'indicizzazione. Un problema può richiedere Retry; il contenuto già salvato viene conservato e un esito incompleto non viene presentato come pieno successo. Il piano precedente già prevedeva questi esiti, non garantiva retry automatici. |
| Pulizia Memory dopo sync | Chiamata diretta nel flusso esistente | Stesso effetto funzionale, con meno coordinamento interno. Rimangono il recupero dopo interruzione, i limiti dell'ambito controllato e l'esclusione di errori di connessione o cleanup del solo Catalog. |
| Verifica semantica | Nessuna nuova valutazione generale o revisione obbligatoria a ogni Save | La responsabilità del significato resta allo specialista; i test verificano contratti e casi mirati. La validazione strutturale non garantiva la correttezza del dominio neppure nel piano precedente. |
Sono invariati il riepilogo Memory, le categorie ammesse, i gate esistenti, la
ricerca ibrida e i collegamenti, le correzioni persistenti dei conflitti e la
protezione delle modifiche manuali. Le fonti vengono aggiornate su richiesta come
già concordato. Le scritture deliberate dal core stesso restano supportate tramite
lo stesso servizio: l'assenza di amministrazione concomitante non le elimina.
Non è prevista una rinuncia alle capacità di ricerca o al contenuto delle Evidence.
Conservare gli stessi contenuti tipizzati, ambiti e indicizzazione evita una perdita
di qualità dovuta a un taglio di funzionalità, ma l'equivalenza del nuovo percorso
deve essere verificata. Il formato Markdown e un editor più semplice non sono,
da soli, una garanzia di qualità o di assenza di errori.
## Riesame di tutte le decisioni Q1–Q15
| Decisione | Esito della revisione | Approccio più semplice e conseguenza |
| --- | --- | --- |
| Q1 — Riepilogo Memory | Mantengo | Un riepilogo finale editabile, con selezione di cosa salvare. Nessuna approvazione ripetuta per ogni card durante la sessione. |
| Q2 — Aggiunte e aggiornamenti | Mantengo | Mostrare contenuto risultante e record sostituito. La somiglianza non avvia fusioni automatiche; la cancellazione semantica resta esplicita nel CRUD. |
| Q3 — Consumo nei gate | Mantengo | Usare i gate già previsti. Nessun nuovo percorso di approvazione per la sola consultazione di una Memory; exemplar consultativi. |
| Q4 — Conflitti Memory/Evidence | Semplifico l'esecuzione | La scelta mostra quale archivio cambia e con quale testo. Chiamare lo stesso servizio di salvataggio del CRUD, con un esito unico; niente secondo sistema di pubblicazione. Le correzioni deliberate dal core restano un caso da supportare. |
| Q5 — Dipendenze fisiche eliminate | Mantengo, con chiamata diretta | Alla fine della sincronizzazione fisica riuscita, chiamare la pulizia Memory nello stesso flusso. Mostrare le conseguenze nella conferma della sincronizzazione già esistente e il conteggio finale. Non introdurre bus di eventi o un nuovo controllo continuo del DWH. |
| Q6 — Verifiche | Mantengo il perimetro limitato | CRUD, filtri, persistenza, cancellazioni, recupero dall'errore e casi mirati di ricerca. Nessuna valutazione qualitativa generale a ogni salvataggio, nessun secondo modello giudice obbligatorio. |
| Q7 — Evoluzione interna | Mantengo | Riutilizzare componenti e servizi dell'installazione. Nessun framework esterno o ulteriore servizio per governare Memory. |
| Q8 — Ibrido e collegamenti | Mantengo entrambe le capacità | Riutilizzare la ricerca ibrida; collegamenti in una lista modificabile ed espansione limitata nel core. Nessun database a grafi, editor visuale di grafi o deduzione automatica di una rete di relazioni. Le capacità restano nel progetto attuale. |
| Q9 — PostgreSQL per Memory | Mantengo | Il database è già locale all'installazione; card, collegamenti e dipendenze restano coordinati. Qdrant è ricostruibile. Il flusso Memory non richiede un autore esterno indipendente. |
| Q10 — Cura dei collegamenti | Mantengo | Gestirli nello stesso riepilogo e dettaglio della card. Cancellare una card elimina i collegamenti incidenti e conserva le altre card. |
| Q11 — Archivio Evidence | Rivedo la soluzione tecnica | Draft esterne indipendenti e Evidence raffinate in file locali persistenti. Togliere commit/push dal CRUD. La modifica locale non aggiorna automaticamente la fonte dello specialista. La proposta PostgreSQL autorevole per Evidence è ritirata. |
| Q12 — Save e sessioni aperte | Adatto agli editor esterni | Dopo il salvataggio dei file, un comando manuale consolida e attiva le modifiche; l'operatore completa poi commit/push. Nessun workflow editoriale aggiuntivo, watcher, aggiornamento delle sessioni aperte o modalità manutenzione. |
| Q13 — Fonte cambiata e cura manuale | Mantengo, con confronto semplice | La correzione manuale prevale finché una persona decide altrimenti. Un cambiamento della fonte collegata rende disponibile il confronto; non serve dimostrare automaticamente una contraddizione semantica. Le cancellazioni non vengono annullate dalla rigenerazione. |
| Q14 — Creazione manuale | Mantengo tramite file | Un nuovo Markdown secondo l'esempio documentato consente una Evidence manuale con provenienza dichiarata e senza documento esterno obbligatorio. Il flusso principale draft dello specialista → raffinamento locale rimane disponibile e indipendente. |
| Q15 — Refresh delle fonti | Mantengo | Acquisizione e raffinamento delle fonti aggiornate su richiesta esplicita. Consolidamento e recall usano il contenuto locale; non cercano nuove versioni remote. |
Restano confermati due link autonomi sotto Database management, CRUD completo con
filtri, assenza di storico aggiuntivo delle Memory e assenza di vincoli sulle
sessioni di sviluppo già esistenti.
## Riduzioni trasversali del piano
**Un'operazione alla volta, con esito comprensibile.** Save per Memory e
consolidamento manuale per Evidence validano e aggiornano l'indice in sequenza.
L'interfaccia mostra operazione in corso,
completata oppure errore con azione di recupero. Non espone un workflow editoriale
di stati `draft`, `approved`, `published` per il normale CRUD. La draft dello
specialista è il documento di ingresso della preparazione, non un secondo pulsante
di salvataggio dell'editor delle unità locali.
**Recupero minimo dopo errore.** Se l'archivio è stato aggiornato ma Qdrant no, va
detto e deve essere possibile riprovare, per Evidence rieseguendo il comando.
Serve un'indicazione persistente del lavoro
incompleto, sufficiente anche per ripulire una card già cancellata dopo un riavvio.
La ricerca non deve usare contenuti rimossi o superati alla domanda successiva.
Il piano non imponeva già una outbox o nuovi worker: la revisione rende esplicito
che non sono richiesti né code generiche né sincronizzazioni continue.
**Pulizia schema diretta e ripetibile.** Il flusso esistente del Catalog può
richiamare Memory dopo l'applicazione dello schema. Occorre coprire il crash fra
le due scritture: conservare la pulizia pendente sul run oppure verificare di nuovo
le dipendenze contro lo snapshot fisico riuscito e il suo ambito. Ricalcolare solo
il nuovo diff perderebbe le rimozioni già applicate. La rimozione manuale di
metadati Catalog o un errore di connessione non autorizzano a cancellare Memory.
**Nessuna gestione delle sessioni amministrate contemporaneamente.** Non progettare
aggiornamenti a caldo, ripristino dei contesti già letti, invalidazione dello SQL,
notifiche alle sessioni o generazioni aggiuntive per lettori paralleli. Rimangono le
scritture esplicitamente richieste dalla sessione stessa: il riepilogo Memory e una
correzione Evidence approvata chiamano il medesimo servizio e ne gestiscono l'esito
prima di proseguire. Questo caso non richiede coordinare tutte le altre sessioni.
**Pagine essenziali.** Ricerca testuale, filtri e lista completa permettono di trovare
i contenuti; la ricerca semantica appartiene anzitutto al core. Memory conserva le
form; Evidence espone dettaglio, percorsi e istruzioni per la modifica esterna.
Hash e manifest restano gestiti dal sistema. Si riusano i controlli di accesso
esistenti; chi modifica i file necessita dei permessi sul relativo filesystem,
senza accesso a PostgreSQL o un nuovo sistema generale di ruoli.
## Conseguenze delle azioni da rendere visibili
| Azione | Conseguenza da comunicare |
| --- | --- |
| Save riuscito | Il contenuto corrente è conservato ed è disponibile per le successive elaborazioni. |
| Save con errore dell'indice | Il contenuto è conservato; la disponibilità alla ricerca non è completata. Retry completa il lavoro senza richiedere di riscrivere la modifica. |
| Salvataggio nell'editor esterno | Cambia il file, ma non aggiorna il recall. Una copia esterna va prima riportata nell'archivio dell'installazione. |
| Consolidamento Evidence | Acquisisce aggiunte, modifiche e cancellazioni locali; verifica la struttura e, se valida, aggiorna metadati e indice. Un errore indica cosa correggere; si riesegue il comando dopo la correzione o un errore tecnico. |
| Commit e push manuali | Versionano e trasferiscono al repository remoto i file consolidati. Un errore Git si risolve manualmente; il consolidamento locale già riuscito non viene annullato. |
| Delete di una Memory | La card e i collegamenti che la coinvolgono vengono rimossi; le altre card restano. |
| Delete di una Evidence | L'unità locale non viene più usata né ricreata automaticamente; la draft originale e le altre unità derivate restano. |
| Refresh sources | Si acquisiscono nuove versioni delle fonti e si preparano le Evidence interessate; le correzioni locali protette non vengono sovrascritte. |
| Sincronizzazione fisica | Le Memory dipendenti da elementi effettivamente eliminati vengono cancellate; l'ambito e il conteggio dell'effetto sono visibili. |
| Preprocessing Clear | Si eliminano i dati derivati previsti dal comando; le Evidence canoniche locali e le Memory rimangono. |
Non sono necessarie conferme ripetute su Save. Per le cancellazioni si usa una
conferma concreta sull'oggetto e sulle conseguenze, integrando gli effetti nella
conferma già presente quando l'azione è una sincronizzazione distruttiva.
## Cosa va comunque implementato
Il raffinamento locale e la scrittura di Markdown canonico/manifest esistono in
`harness/tht/evidence/authoring.py`. Preparano e validano gli output prima della
sostituzione con staging e rollback; non eseguono commit/push. Va separato il
requisito di Git worktree dalla preparazione del contenuto e va integrata
l'acquisizione delle draft esterne.
La materializzazione corrente in `backend/src/workspaces/evidence/materialization.ts`
è invece ricostruita da una revisione Git: non è già l'archivio locale scrivibile
proposto. Il renderer e il preprocessing devono consumare il nuovo archivio
persistente. Una modifica locale non deve richiedere un nuovo commit della fonte.
Si riusa l'attivazione Evidence esistente dove serve a verificare un candidato e
a recuperare da errori; l'assenza di lettori contemporanei non rende atomici file
e Qdrant. La mutazione resta circoscritta alle Evidence e preserva Schema, relazioni,
LSH e Memory. Il suo successo non cancella blocchi di readiness del Catalog.
Le verifiche prioritarie coprono il percorso draft → raffinamento → elenco/CRUD
locale → ricerca, persistenza dopo riavvio, retry dopo errore, mancata ricomparsa
delle unità eliminate, refresh con correzioni locali, isolamento dei workspace e
conservazione dell'archivio locale dopo Clear. Le prove di aggiornamento live da
amministrazione vengono eliminate; resta la verifica delle correzioni deliberate
dal core stesso.
L'ordine resta Memory, Evidence e integrazione finale. I dettagli degli incrementi
nei due piani sono lavoro interno; per l'utente rimangono due gestioni autonome.
+343
View File
@@ -0,0 +1,343 @@
# M1 — Archivio autorevole e amministrazione delle Memory Card
Data: 2026-09-08. Stato: M1 implementato e verificato localmente;
confini di test confermati dal proprietario il 2026-09-08.
Primo incremento del progetto Memory management. Attua le decisioni già approvate
nel piano del 2026-09-08 e nell'ADR 0018. Le scelte tecniche di dettaglio qui
proposte derivano dalla ricognizione del runtime. Gli esiti dell'implementazione
sono riportati nel [rapporto di verifica](2026-09-08-memory-m1-validation.md).
## Problem Statement
L'amministratore deve poter trovare, leggere e curare tutta la conoscenza
riutilizzabile di un workspace: chiarimenti di dominio, regole SQL, domande
risolte ed errori compresi da evitare. Oggi manca una pagina amministrativa
dedicata e l'archivio è frammentato: le Memory sono registrate in JSONL, mentre
gli exemplar delle domande risolte sono indicizzati attraverso un percorso distinto.
Questa situazione non offre un unico archivio completo di card, collegamenti e
dipendenze strutturate. La disponibilità dell'indice non deve determinare se una
card è consultabile o modificabile. Una modifica o cancellazione deve inoltre
impedire che una ricerca successiva utilizzi contenuti superati, anche se
l'aggiornamento dell'indice fallisce.
## Solution
Consegnare la pagina **Memory management** in Administration e un archivio
PostgreSQL autorevole, appartenente al modulo Memory. La pagina consente elenco
completo, ricerca testuale, filtri, dettaglio, creazione, modifica, cancellazione
e gestione dei collegamenti. I contenuti restano consultabili con embedding o
Qdrant indisponibili, purché PostgreSQL sia disponibile.
Un salvataggio aggiorna insieme card, collegamenti e dipendenze, poi propaga la
modifica a Qdrant. L'amministratore vede se il contenuto è stato salvato e se è
disponibile al recall. Se la propagazione fallisce può riprovarla, anche dopo un
riavvio. I contenuti rimossi o superati non sono utilizzati dal recall.
M1 consegna l'amministrazione e la coerenza dell'archivio. La ricerca ibrida con
espansione dei collegamenti e il nuovo riepilogo del workflow sono gli incrementi
M2 e M3, entrambi ancora obbligatori per completare il progetto Memory.
## User Stories
1. Come amministratore, voglio aprire Memory management da Administration, così
da curare la conoscenza senza avviare una sessione.
2. Come amministratore, voglio scegliere esplicitamente il workspace da
amministrare, così da sapere a quale archivio appartiene ogni operazione.
3. Come amministratore, voglio elencare tutte le card del workspace, così da
raggiungere anche quelle che non compaiono nel recall semantico.
4. Come amministratore, voglio cercare per testo o identificatore e ordinare i
risultati, così da trovare una card senza conoscerne la formulazione esatta.
5. Come amministratore, voglio combinare filtri per famiglia, concetti, riferimenti
a tabelle o colonne, provenienza e aggiornamento, così da restringere l'intero
archivio prima della paginazione.
6. Come amministratore, voglio leggere contenuto completo, ambito, motivazione e
provenienza disponibile, così da capire quando una card è applicabile.
7. Come amministratore, voglio creare una card manuale senza inventare una sessione
o una decisione di origine, così da registrare conoscenza curata direttamente.
8. Come amministratore, voglio rappresentare chiarimenti, regole SQL, domande
risolte ed errori compresi, così da conservare i contenuti concordati.
9. Come amministratore, voglio conservare domanda, SQL e contesto di un exemplar,
così da distinguerlo da una regola generale.
10. Come amministratore, voglio associare dipendenze esplicite a database, tabelle
e colonne, così da non affidare l'identificazione degli oggetti al testo libero.
11. Come amministratore, voglio correggere i campi consentiti dalla famiglia e
annullare una modifica non salvata, così da controllare il contenuto corrente.
12. Come amministratore, voglio ricevere errori di validazione comprensibili senza
perdere il testo inserito, così da poterlo correggere.
13. Come amministratore, voglio creare, modificare e cancellare collegamenti con
destinazione e significato espliciti, così da curare le relazioni fra card.
14. Come amministratore, voglio salvare card e modifiche correlate come un'unica
operazione, così da non lasciare collegamenti o dipendenze parziali.
15. Come amministratore, voglio cancellare una card e i suoi collegamenti
incidenti conservando le altre card, così da rimuovere solo il contenuto scelto.
16. Come amministratore, voglio ritrovare le modifiche dopo riapertura della pagina
e riavvio del servizio, così da verificare che il salvataggio sia persistente.
17. Come amministratore, voglio consultare e curare l'archivio quando embedding o
Qdrant sono indisponibili, così da proseguire il lavoro amministrativo.
18. Come amministratore, voglio distinguere archivio vuoto e archivio non
disponibile, così da non interpretare un guasto come perdita dei dati.
19. Come amministratore, voglio distinguere salvataggio fallito e contenuto
salvato con indicizzazione incompleta, così da scegliere il recupero corretto.
20. Come amministratore, voglio riprovare una propagazione incompleta anche dopo
un riavvio o una cancellazione, così da completare la pulizia dell'indice.
21. Come reviewer, voglio che una nuova ricerca escluda card eliminate o contenuti
superati, così da ricevere soltanto conoscenza corrente.
22. Come reviewer, voglio che gli exemplar rimangano consultativi e che una Memory
recuperata non costituisca approvazione, così da conservare il controllo del workflow.
23. Come amministratore, voglio che reindicizzazione e preprocessing rispettino le
cancellazioni e le correzioni, così da non doverle ripetere.
24. Come operatore dell'installazione, voglio preparare lo schema e configurare
l'accesso Memory con i meccanismi esistenti, così da avviarlo senza nuovi servizi.
25. Come proprietario del workspace, voglio che API e comandi rispettino il
contesto autorizzato, così da evitare accessi o collegamenti fra archivi diversi.
26. Come proprietario del workspace, voglio che il CRUD Memory lasci invariati
Evidence e metadati del database, così da mantenere distinte le responsabilità.
## Implementation Decisions
### Responsabilità e punti d'ingresso
- Il modulo Memory del harness possiede modello, validazione, repository,
mutazioni, collegamenti, dipendenze e coerenza delle proiezioni. PostgreSQL è
autorevole; Qdrant contiene una proiezione ricostruibile.
- I comandi Memory esistenti diventano adattatori del medesimo servizio. La
superficie viene completata con creazione manuale, gestione dei collegamenti,
elenco delle propagazioni incomplete e retry. Nessuna logica di persistenza
Memory viene duplicata nel backend.
- Il backend espone API amministrative attraverso il runner del harness,
associando principal attendibile e configurazione del workspace alla richiesta.
Si conservano opzioni di configurazione per comando e output JSON puro.
- La ricognizione conferma che il harness usa già SQLAlchemy e PostgreSQL per le
sessioni. Se ne riusano i meccanismi adatti, mantenendo separati modello,
migrazioni e proprietà dei dati Memory. Il repository in memoria del Metadata
Catalog è un test double del Catalog, non il modulo Memory.
### Modello e transazioni
- La card ha identità stabile, workspace, famiglia/contenuto, titolo o soggetto,
ambito, motivazione, provenienza, concetti e date di creazione/aggiornamento.
Le domande risolte conservano anche domanda, SQL e contesto. L'identità di una
card manuale non dipende da una sessione né dal solo testo.
- Il modello rappresenta le quattro categorie di contenuto approvate senza
imporre quattro nuovi kind vettoriali o la corrispondenza con i tipi del ledger.
Le origini manuali sono distinguibili; sessione e decisione sono riferimenti
opzionali quando effettivamente disponibili.
- I collegamenti sono record propri del modulo con sorgente, destinazione e
significato. Le due card devono esistere nello stesso workspace. La cancellazione
di una card elimina i collegamenti incidenti, senza propagarsi alle altre card.
- Le dipendenze identificano esplicitamente database, schema, tabella e colonna
secondo l'ambito applicabile. Il contratto consente il futuro confronto con
lo schema fisico. Un cleanup dei metadati Catalog non deve poter cancellare
card attraverso una cascata implicita di chiavi esterne.
- Le tabelle logiche necessarie sono card, collegamenti, dipendenze e stato
operativo delle proiezioni. Card e modifiche correlate si aggiornano nella
stessa transazione. Una validazione o scrittura fallita non lascia aggiornamenti
parziali. Non si conserva una storia delle revisioni del contenuto.
- Le modifiche ordinarie non spostano una card in un altro workspace. Tutti gli
identificatori ricevuti vengono verificati nel workspace dell'operazione,
inclusi estremi dei collegamenti e riferimenti delle azioni di retry.
### Salvataggio, cancellazione e recall
- Il servizio valida l'intera mutazione, registra il nuovo stato autorevole e
il lavoro di propagazione nella stessa transazione PostgreSQL, poi aggiorna
Qdrant nello stesso flusso di salvataggio. Si completa un'operazione alla volta;
non occorrono una coda generale, un worker o una sincronizzazione continua.
- Lo stato persistente è sufficiente a distinguere la proiezione del contenuto
corrente da una proiezione precedente e a ritentare l'azione dopo un riavvio.
Può usare una versione tecnica o un'impronta interna; non è una cronologia
editoriale né un ulteriore stato che l'utente debba gestire.
- Un risultato Qdrant è utilizzabile solo se corrisponde a una card autorevole
corrente del workspace e a una proiezione valida. Il contenuto restituito
viene dall'archivio autorevole. La verifica si applica anche agli exemplar.
In assenza di verifica autorevole il recall non restituisce il vecchio payload.
- Dopo il commit PostgreSQL, una propagazione fallita lascia il contenuto
consultabile nell'amministrazione e la sua proiezione non utilizzabile dal
recall fino al recupero. L'esito distingue chiaramente questo caso da un
salvataggio fallito prima del commit.
- La cancellazione rimuove card, collegamenti incidenti e dipendenze e conserva
soltanto i dati operativi necessari a eliminare la proiezione. La card non è
più richiamabile anche se il punto Qdrant esiste ancora. La pulizia pendente
resta raggiungibile dalla pagina, senza richiedere il dettaglio della card eliminata.
- Il retry è esplicito e ripetibile. Usa lo stato corrente del repository, non
il contenuto di una vecchia richiesta. Un retry superato non può sovrascrivere
una correzione successiva né ricreare una card cancellata.
- La ricostruzione degli indici Memory e solved-question usa esclusivamente
le card autorevoli. Non reimporta automaticamente il registro JSONL, i payload
Qdrant o le sessioni di origine. Il preprocessing delle reference mantiene
la separazione delle collezioni stabilita nell'ADR 0017.
- Anche i produttori attuali di Memory ed exemplar scrivono attraverso il
servizio autorevole. Si adeguano promozione, salvataggio singolo e percorso di
finalizzazione quanto necessario a evitare scritture dirette al solo indice.
Un errore successivo al commit della sessione non deve annullarne la finalizzazione;
l'esito e il recupero Memory restano espliciti. Questa transizione non introduce
il nuovo riepilogo di approvazione previsto da M3.
### API, autorizzazione e configurazione
- Il contratto amministrativo comprende elenco, dettaglio, creazione,
aggiornamento, cancellazione, manutenzione dei collegamenti, stato delle
propagazioni incomplete e retry. Le mutazioni restituiscono identità interessata,
esito del salvataggio ed esito della propagazione; gli errori non espongono segreti.
- L'elenco restituisce pagina, conteggio totale filtrato e ordinamento stabile
con identificatore come discriminante. Ricerca testuale e filtri combinabili
agiscono sull'intero archivio prima della paginazione, senza embedding.
- Il backend distingue input invalido, accesso negato, record assente nel
workspace richiesto, archivio indisponibile e propagazione incompleta dopo
salvataggio. Un archivio indisponibile non produce una lista vuota riuscita.
- Si riusano autenticazione, controlli di accesso e trasmissione del principal.
Il catalogo attuale non ha un permesso Memory dedicato: la proposta è aggiungere
la capability amministrativa Memory al ruolo admin esistente, senza introdurre
ruoli nuovi. Il controllo copre anche letture amministrative e retry.
- Le operazioni amministrative e i comandi esposti non permettono bypass del
controllo nel harness. Le scritture già previste dal workflow conservano il
proprio contesto autorizzato di sessione; non diventano CRUD amministrativo
liberamente accessibile a un utente ordinario. Il recall rimane accessibile
secondo le regole del workflow.
- M1 include configurazione della connessione al PostgreSQL dell'installazione,
distribuzione protetta delle credenziali al harness, migrazioni versionate e
privilegi runtime necessari alle sole tabelle Memory. Si riusano i meccanismi
di configurazione generata, segreti e provisioning esistenti; non si usano le
credenziali di lettura del DWH. I nomi fisici di schema, tabelle e parametri
vengono fissati nell'implementazione rispettando questi contratti.
- Le migrazioni sono eseguite dal percorso di preparazione dell'installazione,
non da una richiesta HTTP ordinaria. Schema mancante o non aggiornato produce
un errore operativo comprensibile. La transizione non prevede doppie scritture
permanenti o conservazione del comportamento delle sessioni storiche.
### Pagina amministrativa
- Memory management è un accesso indipendente nell'Administration dell'AppShell,
immediatamente dopo Database management, senza richiedere una sessione attiva
o l'ingresso in Database management.
- La pagina rende esplicito il workspace e offre lista paginata, ricerca,
filtri, ordinamento, dettaglio completo e form. Le modifiche hanno salvataggio,
annullamento e validazione. La cancellazione rende chiari contenuto interessato
e rimozione dei collegamenti, seguendo le convenzioni UI esistenti.
- Il feedback distingue operazione in corso, salvataggio fallito, contenuto
salvato con indice incompleto e operazione completata. Il recupero delle
cancellazioni pendenti è disponibile anche quando la card non compare più in lista.
- I controlli sono accessibili da tastiera e hanno etichette comprensibili.
Chrome e messaggi UI sono in inglese; il contenuto resta nella lingua del workspace.
Hash, versioni tecniche e dettagli delle tabelle non sono esposti nel flusso ordinario.
## Testing Decisions
Confini confermati dal proprietario: usare tre confini già presenti nel
repository, concentrando la maggior parte dei casi sul servizio pubblico Memory
del harness. I test osservano risultati, persistenza ed errori; non vincolano
metodi privati, numero di query o disposizione interna delle tabelle.
### 1. Servizio pubblico Memory e suoi comandi
Usare PostgreSQL reale in testcontainers, come nei test del repository delle
sessioni, e gli adapter vettoriali sostituibili già impiegati nei test di recall
e del ciclo di vita solved-question. Gli embedding dei casi deterministici sono
controllati. Un gruppo mirato con Qdrant reale verifica aggiornamento, cancellazione
e ricostruzione della proiezione; non richiede DWH remoto o un modello generativo.
Questo confine verifica il comportamento di archivio, propagazione e recall:
| Caso | Risultato osservabile richiesto |
| --- | --- |
| Creare e riaprire il repository | La card completa, i collegamenti e le dipendenze sono persistiti; l'origine manuale non contiene sessioni inventate. |
| Salvare una card di ciascuna categoria | Contenuto, ambito e dati specifici sono rappresentabili e leggibili senza dipendere dai tipi del ledger. |
| Cercare un record fuori dalla prima pagina | Filtri combinati, totale e ordinamento si riferiscono all'intero archivio. |
| Fallire una scrittura correlata | Card, collegamenti e dipendenze mantengono tutti lo stato precedente. |
| Indicare una card di un altro workspace | Lettura, mutazione, collegamento e retry non accedono al contenuto estraneo. |
| Cancellare una card collegata | Scompaiono card e collegamenti incidenti; le altre card restano intatte. |
| Rendere embedding o Qdrant indisponibili | Elenco e dettaglio funzionano; il CRUD persiste e distingue la propagazione incompleta. |
| Fallire PostgreSQL prima del commit | Nessun falso salvataggio riuscito e nessun nuovo contenuto propagato. |
| Fallire Qdrant dopo un aggiornamento | Il dettaglio contiene la correzione; il recall esclude il vecchio risultato. |
| Fallire Qdrant dopo una cancellazione | La card non è richiamabile; il lavoro di pulizia resta visibile e recuperabile. |
| Riavviare fra commit e propagazione | Il lavoro incompleto permane e un retry lo completa. |
| Ritentare dopo un errore o un esito incerto | Non si creano duplicati; si applica lo stato corrente senza ripristinare contenuti superati. |
| Indice con punto orfano o versione superata | Recall Memory ed exemplar lo escludono anche se ha il punteggio più alto. |
| PostgreSQL indisponibile durante il recall | Il servizio segnala l'indisponibilità senza servire payload non verificati. |
| Ricostruire dopo modifica o cancellazione | Il contenuto corretto è conservato; nessuna card viene ricreata dalle sessioni o da vecchi indici. |
| Eseguire promozione o finalizzazione corrente | I nuovi contenuti passano dall'archivio; un errore Memory successivo non annulla una sessione già finalizzata. |
| Applicare migrazioni e riavviare | Lo schema è utilizzabile con il ruolo runtime previsto; la preparazione è ripetibile e non richiede privilegi di migrazione nelle richieste ordinarie. |
I test CLI coprono solo l'adattamento che il servizio non prova: parsing, principal,
workspace, esiti macchina e JSON puro. I test di integrazione riusano le convenzioni
L0 del harness. I test di recall esistenti continuano a verificare che decisioni
già registrate nella sessione e famiglie non ammesse non vengano riproposte.
### 2. API amministrative Fastify
Usare l'iniezione HTTP e il runner sostituibile già presenti nei test backend,
seguendo i test delle route Catalog e dell'autorizzazione. Verificare principal
autenticato, admin e utente ordinario; validazione; selezione del workspace;
contratto delle risposte e mappatura degli errori. Includere letture, collegamenti
e retry, non soltanto le mutazioni delle card.
Questi test provano il confine HTTP e il passaggio al harness. Non si considera
il runner simulato una prova della transazione PostgreSQL o della coerenza Qdrant.
La normale policy CSRF dell'applicazione resta applicata alle nuove mutazioni.
### 3. Pagina nell'AppShell
Usare React Testing Library, MSW e le convenzioni dei test di AppShell e Database
management. Verificare ingresso amministrativo, scelta workspace, lista completa,
filtri, dettaglio, form, annullamento, errori, collegamenti e retry dopo cancellazione.
Controllare il comportamento tramite elementi accessibili e contenuto visibile.
Un percorso browser mirato sullo stack reale collega i tre confini: amministratore
autenticato, creazione manuale, modifica, riapertura della pagina, cancellazione
e verifica dell'assenza nel recall. I test browser con API intercettate provano
interazione e presentazione; non vengono dichiarati prova della persistenza.
Non si replica l'intera matrice su tutti e tre i confini. PostgreSQL, indice e
recupero sono verificati nel harness; autenticazione e trasporto nel backend;
interazione e feedback nella UI. Il percorso integrato copre il collegamento reale.
### Verifica della consegna
Eseguire i test interessati e i gate documentati dei layer modificati, inclusi
typecheck TypeScript, lint Python e build documentale strict. Le verifiche con
PostgreSQL, Qdrant e browser reale hanno esito riportato separatamente; se un
servizio necessario manca, il relativo gate resta aperto.
M1 non richiede una valutazione della qualità SQL generata da un LLM. I test
deterministici non sono presentati come prova di tale capacità: gli eventuali
casi reali appartengono agli incrementi che cambiano generazione e workflow.
## Out of Scope
- Ricerca ibrida, nuova selezione per ambito ed espansione dei collegamenti: M2.
- Riepilogo finale modificabile, nuove categorie nei gate e pulizia dopo una
sincronizzazione fisica del Catalog: M3. M1 ne prepara card e dipendenze.
- Authoring, consolidamento e manutenzione Evidence: E1–E3; risoluzione persistente
congiunta dei conflitti fra Memory ed Evidence: X1.
- Revisione storica delle card, snapshot per vecchie sessioni, migrazione dei dati
di sviluppo o compatibilità con il registro JSONL come archivio operativo.
- Aggiornamento a caldo delle altre sessioni, nuove invalidazioni dello SQL già
generato, coordinamento generale dei lettori paralleli e modalità manutenzione.
- Nuovi servizi PostgreSQL o graph database, code generiche, worker e polling continuo.
- Promozione automatica di rifiuti senza spiegazione o scelte occasionali,
consolidamento automatico e benchmark generale della qualità del modello.
- Deploy o pulizia dell'installazione PSD: restano soggetti ai rispettivi piani e gate.
## Further Notes
Fonti: progetto **Memory management** del 2026-09-08; piano comune
**Amministrazione di Memory ed Evidence**; **Revisione di semplicità: Memory ed
Evidence**; glossario di dominio; ADR 0017 sulla separazione delle collezioni e
ADR 0018 su PostgreSQL autorevole e Qdrant per il retrieval.
La ricognizione ha verificato MemoryRecord, recall ordinario, ricerca degli
exemplar, comandi Memory, repository PostgreSQL delle sessioni, runner del harness,
autorizzazione backend e navigazione amministrativa. Il recall ordinario oggi
risolve già i risultati nel registro autorevole, mentre gli exemplar leggono
contenuti dal payload vettoriale: M1 deve uniformare entrambe le garanzie.
Le scelte tecniche da fissare durante l'implementazione sono nomi e DDL delle
tabelle, firma esatta dei nuovi comandi/API e parametri generati di connessione.
Devono rispettare i contratti e i casi di accettazione di questa specifica;
non riaprono le decisioni di prodotto approvate.
La destinazione della specifica è il tracker Gitea canonico di ThothII, con
etichetta **ready-for-agent**. Il proprietario ha confermato i confini di test
e autorizzato la pubblicazione il 2026-09-08. L'implementazione resta da eseguire.
@@ -0,0 +1,103 @@
# M1 — Implementazione e verifica
Data: 2026-09-08. Implementazione locale della
[specifica approvata](2026-09-08-memory-m1-spec.md), associata all'
[issue 27](https://git.tylconsulting.it/mptyl/ThothII/issues/27).
## Risultato
La pagina **Memory management** è disponibile nell'Administration dopo Database
management. Gestisce le quattro famiglie di card, elenco completo, ricerca e filtri,
ordinamento, dettaglio, creazione, modifica, cancellazione, collegamenti e dipendenze.
Richiede un amministratore autenticato e una selezione esplicita del workspace;
non richiede una sessione o un database DWH configurato.
Il harness possiede l'archivio PostgreSQL `thoth_memory`. Card, collegamenti,
dipendenze e lavoro di propagazione sono salvati nella stessa transazione.
La pagina distingue salvataggio fallito e contenuto salvato con indice incompleto,
offrendo retry anche per le cancellazioni. Recall Memory ed exemplar verificano
esistenza, workspace e proiezione corrente nell'archivio prima di restituire contenuto.
Promozione, salvataggio singolo e finalizzazione corrente passano dal servizio
autorevole. Le ricevute della sorgente impediscono duplicati e ricreazione di card
cancellate. Reindicizzazione e preprocessing non importano vecchi payload o sessioni.
L'errore Memory non annulla una sessione già finalizzata; il gate segnala anche
una promozione salvata con indicizzazione incompleta.
Le migrazioni sono versionate, controllate tramite checksum e incluse nel wheel
e nell'immagine core. Il servizio di preparazione `catalog-migrate` le esegue dopo
quelle del Catalog. Il runtime assume il ruolo limitato `thoth_memory_runtime`,
con isolamento del workspace tramite RLS e senza privilegi DDL.
## Verifiche eseguite
| Confine | Esito |
| --- | --- |
| Harness, test senza L0/L2 | 1.134 passati; i 9 test dei percorsi portabili sono stati eseguiti separatamente e sono passati. |
| Servizio Memory, PostgreSQL e Qdrant reali | 17 passati, inclusi CLI, migrazioni, ruolo runtime, isolamento, transazioni, outage, retry, cancellazioni, cambio famiglia e rebuild. |
| Gate Pi | 190 passati, inclusi identità UUID e avviso dopo salvataggio con indice incompleto. |
| Backend | 1.345 passati nella suite completa, 40 esclusi dalle condizioni previste dai test; un test di autenticazione ha superato il timeout sotto carico. Il relativo file è stato rieseguito isolato: tutti i 17 test passati. |
| Frontend | 632 passati, inclusi ingresso dall'AppShell, form, filtri, collegamenti, dipendenze e retry delle cancellazioni. |
| Browser integrato | Passato: autenticazione amministratore, creazione, modifica, riavvio del backend, rilettura, cancellazione e assenza nel recall. |
| Build e tipi | Build backend e frontend, typecheck TypeScript e build documentale strict superati. |
| Lint e diff | Ruff sui file Python modificati e `git diff --check` superati. Il lint globale segnala tre rilievi in file non modificati, elencati sotto. |
Il browser utilizza autenticamente frontend, login locale, Fastify, ThtRunner,
CLI Python, PostgreSQL e Qdrant. Gli embedding sono deterministici e le attività
Pi/sessione estranee al percorso Memory usano le fixture esistenti. Non sono state
intercettate le API Memory. Sono stati usati container temporanei PostgreSQL 16 e
Qdrant 1.18.2, senza accesso a un DWH remoto o a un modello generativo.
Il test browser ha consentito di correggere etichette accessibili instabili nei
campi compilati e la sovrapposizione del pannello di recupero ai comandi del dettaglio.
La selezione del workspace e l'uscita dalla pagina sono bloccate durante le operazioni.
Il lint globale preesistente riguarda soltanto:
- ordinamento import in `harness/tests/test_effective_relationships.py`;
- ordinamento import in `harness/tests/test_p3_dwh_binding.py`;
- uso di `datetime.UTC` in `harness/tht/mschema/catalog_snapshot.py`.
## Riproduzione
Usare Node 24 e le dipendenze installate dei tre layer. Per eseguire il harness
in un ambiente con home non scrivibile si può impostare `THT_HOME` su una directory
di prova. I test dei percorsi portabili devono essere eseguiti senza questo override,
perché verificano deliberatamente la risoluzione dell'home e di `THT_DATA_ROOT`.
```sh
cd harness
THT_HOME=/private/tmp/thothii-m1-test-home .venv/bin/pytest -m 'not l0 and not l2' --ignore=tests/test_portable_paths.py -q
.venv/bin/pytest tests/test_portable_paths.py -q
.venv/bin/pytest tests/memory/test_administration.py -q
npm test
```
```sh
cd backend
npx vitest run
npx tsc --noEmit -p .
npm run build
```
```sh
cd frontend
npx vitest run
npx tsc -b
npm run build
THT_MEMORY_BROWSER_E2E=1 npx playwright test e2e/memory-real.spec.ts
```
Il percorso browser richiede Docker, Python del harness, Go per il bridge di
autenticazione e Chromium di Playwright. Avvia risorse isolate e le rimuove alla
fine. Su macOS il browser deve poter avviare i processi Chromium fuori dalle
restrizioni della sandbox. La build documentale si esegue dalla radice con
`./scripts/build-docs.sh`.
## Stato della consegna
Le modifiche sono nel worktree locale. Nessuno stack già attivo è stato aggiornato
e nessun dato esistente è stato migrato o eliminato. Prima di usare M1 su
un'installazione occorrono il nuovo core e la preparazione `catalog-migrate`.
M2 (retrieval ibrido ed espansione dei collegamenti), M3 (integrazione estesa nel
workflow) ed Evidence management restano incrementi successivi.
+342
View File
@@ -0,0 +1,342 @@
# Progetto: Memory management
Data del piano: 2026-09-08. Aggiornamento 2026-09-09: M1–M3 implementati,
con integrazione X1 per le correzioni persistenti dei conflitti. Risultati e limiti
sono raccolti nel [rapporto X1](2026-09-09-archive-repair-x1-validation.md).
La [revisione di semplicità](2026-09-08-memory-evidence-simplification-review.md)
mantiene il perimetro Memory e precisa un salvataggio sequenziale e una pulizia
diretta dopo sincronizzazione. Non si progetta l'amministrazione contemporanea
all'attività core.
Il progetto realizza il CRUD amministrativo previsto dalla discussione sull'evoluzione
della Memory. Segue le [decisioni comuni di Administration](2026-09-08-memory-evidence-administration.md)
e rimane distinto dal [progetto Evidence management](2026-09-08-evidence-management.md).
## Risultato richiesto
Un amministratore apre Memory management direttamente da Administration, cerca e
filtra l'intero archivio di un workspace e gestisce le card senza avviare una sessione.
La voce è immediatamente sotto Database management, allo stesso livello.
L'elenco comprende le Memory riutilizzabili e gli exemplar `solved_question`,
distinguibili per famiglia. La modifica di un exemplar non riscrive gli artefatti
della sessione da cui deriva e non lo trasforma in una decisione applicabile al gate.
## Contenuti ammessi: decisione del proprietario
Il perimetro concordato il 2026-09-08 comprende quattro categorie di contenuto.
La classificazione descrive il valore della conoscenza; non impone quattro nuovi
kind tecnici o una corrispondenza con i tipi delle decisioni del ledger.
| Contenuto | Cosa conserva | Esempio inventato |
| --- | --- | --- |
| Chiarimento di dominio | Significato riutilizzabile di un termine, con il suo ambito | «In questo workspace, ordine evaso significa che tutte le righe sono state spedite.» |
| Regola SQL | Regola corretta per join, filtri o aggregazioni, con condizioni e motivazione | «Il codice commessa è univoco solo all'interno dell'esercizio: collegare movimenti e commesse usando codice ed esercizio.» |
| Domanda risolta | Domanda, SQL approvato e contesto, come exemplar consultativo | «Totale degli ordini del 2024 per cliente», con la query che lo calcola. |
| Errore da evitare | Errore compreso, motivo e comportamento corretto approvato | «Il join fra ordini e righe moltiplica il totale di testata: calcolare il totale una sola volta per ordine.» |
Il criterio di ammissione è l'utilità per altre domande nello stesso ambito.
Una scelta come «questa volta usa il 2024» non è una regola riutilizzabile; il 2024
può rimanere nel contesto della domanda risolta. Analogamente, la selezione di una
tabella per una domanda non diventa automaticamente una regola di schema linking.
La categoria «errore da evitare» richiede una spiegazione verificata e approvata.
Un timeout, una query rifiutata senza motivo o una proposta non selezionata non
bastano a produrre conoscenza. Quando errore e correzione esprimono la stessa
regola, una sola card conserva la regola e la sua motivazione.
## Formazione, aggiornamento e uso delle card
### Decisioni del primo round di grill-with-docs
Il proprietario ha approvato le tre raccomandazioni il 2026-09-08:
- **Q1, approvazione del salvataggio:** riepilogo finale modificabile, preparato
durante il lavoro. Il reviewer corregge le card e sceglie quali salvare; la
creazione manuale da Administration resta sempre disponibile.
- **Q2, operazioni proposte dal core:** aggiunte e aggiornamenti espliciti. Il
riepilogo distingue una nuova card dalla modifica di una card esistente e ne
spiega il cambiamento. La sostituzione richiede la selezione del reviewer;
la cancellazione semantica resta un'operazione del CRUD amministrativo. La pulizia
automatica dei riferimenti invalidi è disciplinata separatamente da Q5.
- **Q3, momento del consumo:** i chiarimenti sono proposti all'inizio, le regole di
collegamento durante lo schema linking e le regole di calcolo durante la costruzione
SQL. Le approvazioni entrano nei gate pertinenti, senza una domanda separata per
ciascuna card; gli exemplar rimangono consultativi.
Queste sono decisioni di prodotto; i contratti runtime non sono ancora aggiornati.
### Decisioni del secondo round di grill-with-docs
Il proprietario ha approvato i chiarimenti su Q4–Q6 il 2026-09-08:
- **Q4, risoluzione persistente dei conflitti:** il gate propone azioni chiuse e
specifiche per il caso, mostrando record interessati, azione e testo o ambito
risultante. Le opzioni possono confermare l'Evidence e correggere la Memory,
confermare la Memory e preparare una correzione dell'Evidence, oppure precisare
gli ambiti distinti di entrambe. È sempre disponibile «Nessuna proposta è adeguata»,
che richiede una riformulazione. Le modifiche Memory confluiscono nel riepilogo
finale; quelle Evidence seguono l'authoring e la pubblicazione del rispettivo
modulo. Come approvato in Q12, accettare la correzione Evidence con le autorizzazioni
necessarie avvia anche l'attivazione automatica, senza un ulteriore `Publish`.
Il sistema distingue proposte da approvare, aggiornamenti in corso o falliti e
archivio già aggiornato. La sola risoluzione della domanda corrente non esaurisce il flusso.
- **Q5, cancellazione dopo modifiche allo schema:** dopo una sincronizzazione
riuscita dello schema fisico, il backend comunica al modulo Memory gli elementi
rimossi. Il modulo identifica tramite dipendenze strutturate le card non più
valide e cancella record e proiezioni ricercabili. Non si introduce lo stato
«Needs review» per conservarle. Il controllo avviene alla sincronizzazione, senza
scansione continua del DWH o interrogazioni aggiuntive a ogni domanda. Un cleanup
manuale del Catalog o un errore di accesso al database non prova una rimozione
fisica e non avvia questa pulizia. Essa è distinta dalle proposte semantiche del
core in Q2. Oggi mancano sia i riferimenti strutturati a colonne nelle Memory sia
il collegamento fra sincronizzazione e pulizia: devono essere implementati.
- **Q6, verifiche concrete:** lo sviluppatore prepara ed esegue test automatici
funzionali per CRUD, filtri, approvazioni, aggiornamenti e cancellazioni. Quando
cambia la ricerca, verifica casi mirati con card necessarie e card fuori ambito;
quando emerge un errore SQL riproducibile, aggiunge una regressione su dati
controllati confrontando i risultati, senza richiedere un identico testo SQL.
L'esperto di dominio conferma inizialmente regola e risultato atteso soltanto per
i casi reali che lo richiedono. La verifica della generazione necessita di un
modello reale ed è separata dalla suite deterministica: una query scritta a mano
non prova che il modello sappia generarla. Non si introduce una valutazione umana
permanente o un benchmark generale con percentuali di miglioramento promesse.
### Terzo round: direzione tecnica e capacità di ricerca
- **Q7, deciso:** il proprietario ha approvato l'evoluzione interna di ThothII.
Il modulo riusa l'infrastruttura dell'installazione e integra card, CRUD, mutazioni
e recall con i gate; non adotta un framework esterno per governare la Memory.
- **Q8, deciso:** il proprietario ha approvato ricerca ibrida in Qdrant e collegamenti
espliciti fra card gestiti dal core, senza un database a grafi aggiuntivo. Entrambe
le capacità sono incluse nella pianificazione attuale; la presenza dei collegamenti
non è rinviata alla futura comparsa di casi concreti.
La configurazione approvata comprende:
- ricerca semantica e lessicale ibrida in Qdrant, con filtri sull'ambito;
- collegamenti espliciti fra card, proposti e revisionabili, percorsi nel core con
espansione limitata e riordinamento dei risultati insieme a quelli della ricerca;
- persistenza dei collegamenti coordinata con le card, con rimozione dei riferimenti
a contenuti cancellati e rispetto dei confini fra workspace e dei gate;
- nessun servizio di database a grafi aggiuntivo.
La scelta include il grafo logico, senza introdurre un servizio di graph DB.
La qualità non è garantita dalla scelta di un motore: mantenere queste capacità
evita una rinuncia architetturale ai collegamenti, ma non dimostra equivalenza
qualitativa con qualsiasi soluzione basata su graph DB.
Qdrant è il motore di ricerca; l'archivio autorevole è PostgreSQL, scelto in Q9.
Le [query ibride di Qdrant](https://qdrant.tech/documentation/search/hybrid-queries/)
e i [filtri sui metadati](https://qdrant.tech/documentation/search/filtering/)
coprono le capacità di ricerca indicate. La logica dei collegamenti di dominio
nel core è lavoro applicativo da implementare.
### Decisioni del quarto round di grill-with-docs
- **Q9, archivio autorevole:** il proprietario ha approvato PostgreSQL, già presente
nell'installazione, con tabelle proprie del modulo Memory per card, collegamenti
e dipendenze dallo schema. Sostituisce il registro JSONL; Qdrant è l'indice
rigenerabile. Le modifiche correlate vengono coordinate in PostgreSQL e la
propagazione a Qdrant deve gestire esplicitamente errori e cancellazioni.
- **Q10, gestione dei collegamenti:** il core propone i collegamenti insieme alle
card, indicando destinazione e significato. Il reviewer li approva nello stesso
riepilogo finale, senza un gate aggiuntivo. Administration ne consente creazione,
modifica e cancellazione manuali. Quando una card è cancellata vengono rimossi
anche i collegamenti che la coinvolgono, conservando le altre card. I collegamenti
contribuiscono al recupero e non applicano automaticamente i contenuti.
La decisione architetturale è registrata nell'[ADR 0018](../adr/0018-use-postgres-for-memory-and-qdrant-for-retrieval.md).
### Flusso da implementare
Il flusso seguente traduce le decisioni approvate. I payload e l'integrazione con
i gate sono dettagli da definire nell'implementazione, senza altre decisioni di
prodotto pendenti.
1. Durante il lavoro il core individua possibili conoscenze riutilizzabili a partire
da decisioni e artefatti registrati. Una candidata esplicita cosa afferma, dove
vale, perché è utile e su quale correzione o decisione si basa.
2. Prima di proporne il salvataggio confronta la candidata con le card correnti.
Un doppione esatto non richiede una nuova card; una somiglianza semantica non
autorizza da sola a eliminare o sovrascrivere una conoscenza.
3. Alla conclusione del lavoro presenta un riepilogo editabile delle aggiunte e
degli aggiornamenti proposti. Il reviewer può correggere il contenuto, restringere
l'ambito e scegliere cosa salvare; approvare la query non equivale ad approvare
ogni generalizzazione ricavata dalla query.
4. Una correzione alla stessa regola nello stesso ambito propone un aggiornamento
esplicito della card esistente. Regole valide in ambiti diversi restano distinte;
un conflitto irrisolto non viene risolto silenziosamente dal modello.
5. Le card salvate sono subito consultabili in Memory management; la loro
disponibilità al recall segue lo stato di indicizzazione. La scrittura sostituisce
il contenuto corrente senza introdurre una cronologia delle Memory.
La creazione manuale da Memory management resta disponibile in qualsiasi momento
e non dipende dal riepilogo finale di una sessione. La form richiede contenuto e
ambito adeguati alla famiglia e identifica l'origine amministrativa.
Gli exemplar conservano domanda e soluzione approvata come materiale consultativo.
Il loro salvataggio non applica le scelte di quella soluzione a domande successive.
La distribuzione del consumo nei passaggi pertinenti è decisa in Q3. Restano da
definire i payload e l'integrazione con i gate esistenti, compreso il contesto necessario
a proporre una regola di collegamento o di calcolo e a registrarne l'approvazione.
### Scenari per verificare il design
| Evento | Esito atteso |
| --- | --- |
| Il reviewer corregge un join perché il codice commessa si ripete fra esercizi e approva la spiegazione. | Proporre la regola con entrambe le chiavi e l'ambito delle tabelle interessate. |
| Il reviewer chiede di limitare solo la domanda corrente al 2024. | Nessuna regola generale; mantenere il periodo nell'eventuale exemplar. |
| Una query conta più volte lo stesso ordine e la correzione viene spiegata e approvata. | Proporre una card che descrive la granularità corretta e il rischio di duplicazione. |
| Una query fallisce per timeout o una memory non viene selezionata. | Nessuna nuova regola dedotta automaticamente dall'evento. |
| La candidata ripete esattamente una regola già presente nello stesso ambito. | Evitare una nuova card duplicata. |
| Una nuova regola corregge una card dello stesso ambito. | Mostrare la sostituzione proposta prima del salvataggio; conservare poi solo il contenuto corrente. |
| Due regole differenti valgono per processi o tabelle differenti. | Conservare entrambe con ambiti espliciti, senza generalizzarle al workspace intero. |
| Il reviewer risolve un contrasto fra Memory ed Evidence. | Mostrare una correzione esplicita degli archivi; applicare i percorsi distinti per Memory ed Evidence. L'accettazione autorizzata della correzione Evidence avvia anche l'attivazione; indicare esito, operazione in corso o errore. |
| Una sincronizzazione riuscita accerta la rimozione di una colonna da cui dipende una card. | Cancellare la card dipendente e rimuoverla dai risultati di ricerca. |
| La connessione al DWH fallisce oppure vengono puliti solo metadati del Catalog. | Non interpretare l'evento come prova di rimozione della colonna e non cancellare Memory per quel motivo. |
## Differenza rispetto al runtime corrente
`harness/tht/memory/core.py` limita `REUSABLE_TYPES` a `concept_clarified`.
Anche il contratto Pi di F2 ammette soltanto questi chiarimenti; gli exemplar
`solved_question` hanno già un percorso distinto di consultazione.
Il perimetro concordato amplia quindi il modulo Memory. Il design deve distinguere
la conoscenza riutilizzabile dall'evento di workflow che l'ha originata, e aggiornare
insieme estrazione, validazione, persistenza, recall e gate. Aggiungere alla whitelist
tutti i tipi delle decisioni SQL o sulle tabelle promuoverebbe anche scelte occasionali
e non realizza il requisito.
## Funzioni
- Elenco paginato e ordinabile, ricerca per testo o identificatore.
- Filtri combinabili per workspace, famiglia/kind, concetti, tabelle e colonne
quando presenti, provenienza e data di aggiornamento.
- Dettaglio completo: titolo, contenuto, ambito, motivazione e provenienza disponibile.
- Creazione manuale di una card, distinguibile da una card prodotta dal workflow;
la creazione manuale non inventa una sessione o una decisione di origine.
- Modifica dei campi consentiti dalla famiglia, con validazione e annullamento.
- Cancellazione del record e rimozione delle sue proiezioni ricercabili.
- Indicazione di contenuti salvati ma non ancora disponibili al recall, con retry
dell'operazione necessaria a renderli disponibili.
L'elenco amministrativo legge i record persistiti senza richiedere embedding o
ricerca per similarità. Un'indisponibilità dell'archivio deve produrre un errore
esplicito, distinguibile da un elenco vuoto. I filtri sono applicati sull'intero
archivio, prima della paginazione, e non sui soli risultati del recall.
## Comportamento delle modifiche
Una modifica sostituisce il contenuto corrente. Non si introducono revisioni storiche,
snapshot dedicati alle vecchie sessioni o migrazioni per conservarne il comportamento.
La cancellazione toglie la card dall'archivio e dal recall ordinario.
La mutazione deve aggiornare o invalidare ogni proiezione interessata. Un errore
dell'indice non può essere presentato come piena disponibilità del nuovo contenuto,
né permettere di usare silenziosamente il contenuto eliminato o sostituito.
La strategia di consistenza e di retry appartiene al design tecnico del modulo.
Il salvataggio esplicito dell'amministratore cura il contenuto condiviso. Il suo
successivo consumo nel core mantiene la semantica della famiglia: le Memory vengono
proposte secondo i gate del workflow, gli exemplar restano consultativi.
## Piano esecutivo
### M1 — Archivio e CRUD amministrativo
Definire nel modulo `harness/tht/memory/` il contratto delle card: identità stabile,
workspace, contenuto, famiglia, ambito, motivazione, provenienza e dati specifici
delle domande risolte. I riferimenti allo schema identificano database, tabella e
colonna senza affidarsi alla sola presenza di nomi nel testo. Le card manuali
non richiedono sessioni inventate.
Implementare un repository PostgreSQL del modulo con card, collegamenti e dipendenze.
La transazione aggiorna insieme il contenuto e le modifiche correlate; la propagazione
a Qdrant avviene nello stesso flusso di salvataggio. Conservare un'indicazione
persistente dell'operazione incompleta, sufficiente anche a ripulire cancellazioni
dopo un riavvio; il recupero usa un retry esplicito. Non servono una coda generale,
un nuovo worker o una sincronizzazione continua. Un risultato indicizzato
con contenuto superato o privo di card autorevole non può essere usato dal recall.
Gli exemplar passano anch'essi dall'archivio autorevole. La transizione dal registro
JSONL non introduce scritture doppie permanenti o compatibilità storica delle sessioni.
Esporre attraverso il backend elenco filtrato prima della paginazione, dettaglio,
creazione, aggiornamento, cancellazione, gestione dei collegamenti ed esito della
propagazione. Le operazioni chiamano la logica del harness e applicano controllo
amministrativo e isolamento del workspace. La UI legge il repository attraverso
queste API anche quando il servizio di embedding o Qdrant è indisponibile.
Consegnare Memory management nell'AppShell con form, contenuto completo, gestione
dei collegamenti e feedback di salvataggio/indicizzazione. Verificare persistenza
PostgreSQL, rollback delle mutazioni correlate, aggiornamento e rimozione dal recall,
retry dopo errore dell'indice, filtri sull'intero archivio e autorizzazioni. Una
cancellazione elimina i collegamenti incidenti conservando le altre card.
### M2 — Ricerca ibrida e collegamenti
Estendere l'adapter Qdrant alla ricerca dense e lessicale della Memory e applicare
l'ambito anche ai risultati raggiunti attraverso collegamenti. Le card iniziali
alimentano l'espansione limitata nel core; deduplicazione, gestione dei cicli e
limiti espliciti impediscono una visita incontrollata dell'archivio. I risultati
vengono riordinati insieme e risolti contro il contenuto autorevole corrente.
Verificare card attese, esclusioni per ambito, cicli, collegamenti verso card rimosse
e rigenerazione dell'indice da PostgreSQL. Quando si verifica il recupero effettivo,
usare il percorso di embedding e ricerca configurato su un indice isolato: un fake
che restituisce gli ID predisposti verifica soltanto il contratto applicativo.
La separazione fra le collezioni Reference e Memory rimane quella degli ADR 0017 e 0018.
### M3 — Workflow e sincronizzazione fisica
Aggiornare insieme contratti, CLI, regole Pi e widget necessari al riepilogo finale
modificabile. Il salvataggio applica soltanto card e collegamenti selezionati; le
nuove categorie entrano nei gate pertinenti. Il recupero di una regola non ne
costituisce approvazione, e l'exemplar continua a essere consultativo.
Collegare la sincronizzazione fisica del Catalog alla pulizia delle dipendenze nel
modulo Memory con una chiamata diretta dopo l'applicazione riuscita dello schema,
con copertura del controllo e riferimenti rimossi. Per recuperare un'interruzione
fra applicazione e pulizia, conservarne lo stato pendente oppure verificare di
nuovo le dipendenze contro lo snapshot fisico riuscito e il suo ambito: il nuovo
diff da solo perderebbe le rimozioni già applicate. La pulizia è ripetibile e
non richiede un sistema generale di consegna eventi.
Un confronto parziale non prova la rimozione di elementi fuori dall'ambito controllato.
La pulizia aggiorna archivio, collegamenti e proiezioni senza un'azione manuale ulteriore.
Verificare selezioni e rifiuti nel riepilogo, contenuto manuale, categorie ammesse,
notifica di rimozione fisica, errore di connessione e cleanup del solo Catalog.
Integrare le correzioni che riguardano Evidence nell'incremento congiunto X1, dopo
il completamento del relativo servizio di authoring e attivazione.
L'evoluzione interna è decisa in Q7; ricerca ibrida e grafo nel core in Q8;
PostgreSQL autorevole in Q9; gestione dei collegamenti in Q10. I contratti tecnici
di persistenza, indicizzazione e API devono attuare queste decisioni. Apprendimento
automatico da rifiuti non spiegati e consolidamento automatico restano fuori dal
perimetro concordato; gli errori compresi e approvati rientrano nei contenuti decisi.
## Criteri di completamento
- Accesso amministrativo indipendente da sessioni e da Database management.
- Tutti i record sono raggiungibili con elenco, filtri e paginazione, senza dipendere
dalla disponibilità di embedding e recall semantico.
- Creazione, modifica e cancellazione persistono dopo riapertura della pagina.
- Dopo una mutazione completata il recall usa il contenuto corrente; i record
cancellati non riappaiono dopo reindicizzazione o preprocessing.
- Errori di salvataggio e indicizzazione sono distinguibili e recuperabili.
- API e interfaccia rispettano isolamento dei workspace e accesso amministrativo.
- Nessuna operazione del CRUD modifica Evidence o metadati del database.
- Non vengono richieste compatibilità storica o conservazione delle sessioni esistenti.
- Le quattro categorie concordate sono rappresentabili senza promuovere le scelte
occasionali a regole generali; gli scenari di ammissione verificano il confine.
- La rimozione fisica accertata di una dipendenza elimina le card interessate;
errori di connessione e cleanup del Catalog non vengono scambiati per rimozioni.
- Le scelte sui conflitti producono correzioni persistenti esplicite secondo Q4.
- La verifica rispetta Q6, separando contratti funzionali e casi di generazione reale.
- Il recupero combina ricerca ibrida, filtri d'ambito e collegamenti espliciti fra
card; la gestione del grafo non richiede un servizio di database aggiuntivo.
- Card, collegamenti e dipendenze hanno un'unica fonte autorevole PostgreSQL;
la rigenerazione di Qdrant conserva il contenuto corrente e le cancellazioni.
- I collegamenti sono curabili nel riepilogo e in Administration; cancellare una
card elimina i suoi collegamenti senza cancellare altre card.
@@ -0,0 +1,283 @@
# PRD — Security hardening per Docker personale e server multiutente
**Stato:** bozza da validare con `grill-with-docs`; implementazione rinviata.
**Data:** 8 settembre 2026.
**Owner delle decisioni:** il maintainer di ThothII.
**Ripresa:** [prompt per la prossima sessione](2026-09-08-security-hardening-resume-prompt.md).
Questo documento conserva la survey di sicurezza discussa con il maintainer e propone requisiti,
priorità e criteri di accettazione. Non è una spec approvata, un penetration test, una certificazione
o un'autorizzazione a intervenire sui deploy. Nessun ticket, worktree o intervento applicativo è
stato avviato per questo PRD. Non è prevista un'esecuzione automatica alla scadenza di una data.
La survey aveva come riferimento HEAD `50c546e42d03281082af3679b8b8d7ce0d01aefa`; alla
registrazione di questo documento HEAD è `818563c4085c9ebce69dca2cbc15ed71fe13f381`.
Sono riferimenti per confrontare le revisioni, non attestazioni di un worktree originariamente
pulito. I rilievi derivano da lettura del codice, controlli locali mirati e audit delle dipendenze:
non è stata verificata la configurazione effettiva del server remoto. Alla ripresa ogni rilievo
deve essere riconfermato, chiuso con evidenza oppure riformulato rispetto al codice corrente.
## Problema
ThothII deve avere un perimetro di sicurezza comprensibile e verificabile sia come applicazione
Docker su un Mac o PC personale, sia come installazione server condivisa con autenticazione
built-in oppure OIDC. Le protezioni esistenti non bastano, da sole, a dimostrare isolamento fra
utenti, contenimento dell'agente e controllo delle informazioni inviate ai modelli.
In particolare, autenticare un utente non equivale ad autorizzarlo su una sessione o sui dati di
un workspace; un gate del workflow non equivale a una sandbox; cifrare un vault non protegge
dal furto congiunto del vault e della sua chiave.
## Risultato desiderato
L'operatore sceglie un profilo supportato, riceve una diagnosi chiara delle configurazioni
incompatibili e può verificare i controlli con prove ripetibili. L'utente accede soltanto alle
risorse consentite. Credenziali e dati sorgente attraversano esclusivamente i confini approvati.
I rischi residui sono espliciti e accettati dal maintainer, non dedotti dal passaggio dei test.
### Profili da confermare
| Profilo | Confine e aspettativa proposta | Decisione ancora necessaria |
| --- | --- | --- |
| Docker personale, singolo utente | Host fidato; interfaccia pubblicata su loopback; servizi interni non pubblici | Eventuale modalità senza login, accesso LAN e deroghe TLS ammesse |
| Server multiutente, built-in | HTTPS, account distinti, autorizzazione server-side e sessioni separabili | Risorse condivise, ruoli, recupero account e requisiti MFA |
| Server multiutente, OIDC | Stesse garanzie applicative; IdP come fonte di identità e attributi concordati | Mapping dei gruppi, scadenze, revoca e comportamento con IdP indisponibile |
Il luogo del deploy, la modalità di autenticazione e il backend di persistenza sono assi distinti.
In particolare, “Docker in locale” non significa necessariamente modalità di autenticazione
`local`, né richiede necessariamente persistenza filesystem. “Multiutente” non implica una
piattaforma SaaS multi-tenant: va deciso se gli utenti condividono gli stessi diritti sui dati.
### Confini di fiducia
- Browser e richieste HTTP rispetto a reverse proxy, backend e identità autenticata.
- Utente A rispetto a sessioni, stream, artefatti e risorse dell'utente B.
- Processo Pi e contenuti non fidati rispetto a filesystem, credenziali e altri processi.
- ThothII rispetto a DWH, IdP, provider LLM ed endpoint embedding.
- Dati operativi rispetto a log, tracce Pi, indici, esportazioni e backup.
- Immagini e dipendenze distribuite rispetto al runtime installato.
Considerare almeno: richieste non autenticate, utente autenticato che tenta accessi non consentiti,
contenuti DWH/documentali che inducono l'agente ad azioni improprie, segreti esposti accidentalmente
e richieste che consumano risorse eccessive. La compromissione dell'amministratore dell'host non
viene considerata risolvibile dal solo hardening applicativo; va esplicitato cosa resta esposto.
## User stories
1. Come operatore personale, voglio avviare Docker senza pubblicare involontariamente il servizio sulla rete.
2. Come operatore server, voglio che combinazioni di autenticazione, esposizione e storage non sicure siano rifiutate con un rimedio leggibile.
3. Come utente, voglio che conoscere l'ID di una sessione altrui non mi permetta di leggerla o modificarla.
4. Come amministratore, voglio definire quali workspace e dati sono condivisi e quali richiedono autorizzazioni distinte.
5. Come utente, voglio che login built-in e OIDC producano le stesse garanzie di autorizzazione applicativa.
6. Come amministratore, voglio che logout, revoca e rimozione dei diritti abbiano tempi di efficacia dichiarati, inclusi gli stream aperti.
7. Come responsabile dei dati, voglio che i valori sensibili non raggiungano un destinatario non autorizzato attraverso il grounding o la generazione.
8. Come operatore, voglio verificare identità e cifratura del DWH, ricevendo un errore se la verifica richiesta fallisce.
9. Come operatore, voglio che Pi disponga soltanto delle risorse necessarie al lavoro approvato.
10. Come utente, voglio continuare a usare i gate umani e il resume senza perdere le protezioni di isolamento.
11. Come responsabile dei dati, voglio sapere quali contenuti sono persistiti e per quanto tempo, anche fuori dagli artefatti di workflow.
12. Come operatore, voglio diagnosticare errori senza leggere API key, password, token o campioni sensibili nei log.
13. Come utente di un server condiviso, voglio che una richiesta costosa di un altro utente non esaurisca tutte le risorse disponibili.
14. Come operatore dietro proxy, voglio limiti e controlli HTTP che usino correttamente l'origine delle richieste.
15. Come maintainer, voglio aggiornamenti delle dipendenze verificabili e distribuzioni riproducibili.
16. Come operatore, voglio backup ripristinabili con protezioni e limiti della cifratura espliciti.
17. Come maintainer, voglio test negativi che dimostrino il rifiuto degli abusi, oltre ai percorsi leciti.
18. Come operatore, voglio istruzioni di adozione e rollback per ogni cambiamento che coinvolge dati o configurazioni esistenti.
## Registro dei rilievi della survey
Le priorità sono proposte, condizionate al profilo e all'esposizione. **P0** indica un candidato
bloccante per il rilascio del profilo interessato, **P1** il successivo hardening e **P2** il
consolidamento. Non sono punteggi CVSS né affermazioni di sfruttabilità già dimostrata.
| ID | Evidenza o limite osservato nella survey | Rischio da verificare | Priorità proposta |
| --- | --- | --- | --- |
| SEC-01 | Senza session storage PostgreSQL il resolver usa il principal locale e il repository filesystem usa una radice condivisa | Attribuzione e separazione delle sessioni in combinazioni multiutente non supportate | P0 server; verificare prima i vincoli di configurazione già presenti |
| SEC-02 | Pi è un processo figlio del core; il gate dichiara di non essere una sandbox | Accesso a risorse o credenziali oltre il compito, anche per effetto di contenuti non fidati | P0 se si promette isolamento fra utenti non fidati; P1 personale |
| SEC-03 | Nel percorso DWH diretto era emersa una discontinuità fra configurazione TLS resa dal deployment e configurazione consumata dal connettore | TLS richiesto ma non applicato o verificato come atteso | P0 per connessioni che richiedono TLS; riconfermare per ogni driver |
| SEC-04 | Il grounding LSH restituisce valori reali; l'applicazione del Sensitive Data Flag a questo percorso è un follow-up esplicitamente rinviato | Valori sensibili nel contesto di un modello non autorizzato | P0 se quel percorso usa dati sensibili e un destinatario non autorizzato |
| SEC-05 | Pi ha una propria directory di sessione; la survey non ha trovato l'opzione che disabilita la persistenza nel lancio esaminato | Tracce aggiuntive rispetto agli artefatti dichiarati, con retention non verificata | P1; presenza e contenuto effettivo delle tracce da verificare |
| SEC-06 | Gruppi OIDC fotografati al login; autenticazione SSE all'apertura, senza evidenza di chiusura alla revoca | Permessi o stream utilizzabili oltre il tempo di revoca atteso | P1 server; elevare se il requisito richiede revoca immediata |
| SEC-07 | La permission di uso delle sessioni consente l'accesso all'elenco dei workspace senza una ACL per workspace osservata | Accesso ai dati più ampio del previsto se i diritti differiscono fra utenti | P0 con diritti differenziati; scelta esplicita altrimenti |
| SEC-08 | Un LIMIT già presente può superare il limite applicativo; fetch completo prima del taglio del risultato | Consumo eccessivo di memoria, DWH, processi e budget modello | P1, con particolare rilievo multiutente |
| SEC-09 | Rate limit basato sull'IP della richiesta; configurazione proxy fidati e hardening degli header da completare/verificare | Contatori condivisi impropriamente, fiducia errata negli header o difese browser incomplete | P1 server |
| SEC-10 | Audit storico con advisory nel runtime Pi; dipendenze Python runtime non interamente vincolate da un lock nella build esaminata | Vulnerabilità dipendenti dalla versione e build non riproducibili | P1; nuova scansione e verifica di raggiungibilità obbligatorie |
| SEC-11 | Core non-root, ma senza tutte le restrizioni osservate nel servizio di manutenzione | Impatto maggiore di un processo compromesso | P1 server, P2 personale salvo rischio specifico |
| SEC-12 | Vault cifrato con chiave locale adiacente; backup esaminato non cifrato come archivio | Copia congiunta di chiave e dati; esportazioni o backup troppo accessibili | P1 se esportati fuori dall'host, altrimenti priorità da concordare |
La survey aveva anche rilevato controlli da preservare: hashing Argon2id, cookie di sessione
opachi con HttpOnly/SameSite, controlli CSRF/origin, OIDC con PKCE e nonce, revoca applicativa,
RLS PostgreSQL, query read-only con timeout, container non-root e assenza di mount del socket
Docker. La RLS dipendente dall'identità impostata dall'applicazione non sostituisce una sandbox
contro codice che possiede le credenziali del database.
## Requisiti e accettazione proposti
Questi sono esiti da validare, non scelte implementative già approvate. Soglie, eccezioni e
semantica dei permessi vanno fissate nel grilling prima di rendere i ticket eseguibili.
### R1 — Profili supportati e proprietà delle sessioni (SEC-01)
- Descrivere la matrice consentita di esposizione, autenticazione e persistenza; rifiutare le combinazioni incompatibili prima di accettare traffico.
- Con due utenti distinti, verificare elenco, lettura, modifica, resume, artefatti e SSE: un accesso fuori dai diritti deve essere rifiutato senza rivelare contenuti.
- Il percorso personale supportato deve continuare a funzionare; non introdurre un fallback silenzioso al principal locale sul server.
### R2 — Contenimento del runtime agente (SEC-02)
- Concordare tool, directory, segreti ed egress necessari a Pi, nonché il livello di fiducia fra gli utenti.
- Dimostrare con fixture controllate che un'azione non autorizzata non accede a risorse di un'altra sessione o a credenziali non necessarie.
- Verificare che il contenimento scelto mantenga funzionanti workflow, gate e resume. Se il confine scelto non garantisce isolamento ostile, documentare e approvare quel limite.
### R3 — TLS DWH end-to-end (SEC-03)
- Per ogni driver supportato, verificare che i parametri TLS arrivino al connettore realmente usato.
- CA valida e nome corretto devono consentire la connessione; CA non fidata, nome errato o downgrade vietato devono fallire quando il profilo richiede verifica TLS.
- Eventuali eccezioni personali devono essere deliberate, visibili e prive di fallback automatico.
### R4 — Controllo della divulgazione dei valori (SEC-04)
- Applicare la policy approvata prima che risultati LSH e altri campioni raggiungano il prompt, non soltanto nella UI.
- Testare Sensitive Data Flag e Model Data Boundary alle interfacce effettive di uscita; verificare che il valore protetto sia assente, non solo che un helper sia stato chiamato.
- Decidere il comportamento con classificazione mancante, nuovi campi, indici già pubblicati e flag cambiati dopo l'indicizzazione.
- Rispettare il gate di owner acceptance già registrato per il follow-up schema-linking; confermarne lo stato prima di implementare.
### R5 — Persistenza, retention e log (SEC-05)
- Inventariare artefatti, tracce Pi, temporanei, log, indici ed esportazioni realmente prodotti.
- Definire abilitazione, contenuto ammesso, accessi, retention e cancellazione per ciascuna classe.
- Una sessione di prova con segreti-canary e dati sintetici deve dimostrare l'assenza dei contenuti vietati; la pulizia deve preservare gli artefatti necessari al resume.
### R6 — Revoca e ciclo di vita dell'identità (SEC-06)
- Concordare tempi massimi distinti per revoca applicativa e variazione dei diritti nell'IdP, senza promettere sincronizzazione istantanea non supportata.
- Verificare logout, revoca amministrativa, scadenza, cambio dei gruppi, stream già aperti e riconnessione.
- Stabilire se e come gestire processi in corso quando cambiano i diritti; definire il comportamento durante indisponibilità delle fonti di validità.
### R7 — Autorizzazione alle risorse condivise (SEC-07)
- Decidere il perimetro condiviso di workspace, DWH, catalogo e amministrazione.
- Se i diritti sono differenziati, applicarli alle API e alle operazioni di creazione/resume, non soltanto al selettore frontend.
- Se tutti gli utenti autorizzati condividono i dati, renderlo un contratto esplicito senza presentarlo come isolamento multi-tenant.
### R8 — Limiti effettivi e disponibilità (SEC-08)
- Fissare limiti per righe, byte, durata, concorrenza e chiamate al modello; valutare quote per utente sul server.
- Verificare SQL con LIMIT assente o eccessivo e risultati voluminosi: il consumo deve essere limitato prima del fetch completo, non soltanto nella risposta.
- Timeout e cancellazione devono rilasciare risorse; una richiesta abusiva non deve impedire il lavoro lecito dell'altro utente entro i limiti concordati.
### R9 — Reverse proxy e difese HTTP (SEC-09)
- Dichiarare i proxy fidati e verificare sia il corretto riconoscimento del client sia il rifiuto di header inoltrati da sorgenti non fidate.
- Verificare rate limit, origini ammesse, cookie e protezioni CSRF nei percorsi diretto e HTTPS dietro proxy.
- Definire una policy per gli header di sicurezza e CSP compatibile con frontend e SSE; testare il comportamento effettivo del browser.
### R10 — Dipendenze e build (SEC-10)
- Rigenerare l'inventario delle dipendenze effettivamente distribuite, includendo Pi, Python e immagini base; separare runtime e sviluppo.
- Per ogni advisory rilevante, registrare versione, percorso, raggiungibilità, rimedio o accettazione motivata. I conteggi dell'audit storico non sono una baseline aggiornata.
- Rendere riproducibile la risoluzione delle dipendenze concordate e verificare un rebuild; evitare aggiornamenti indiscriminati scollegati dai rilievi.
### R11 — Hardening Docker (SEC-11)
- Applicare il minimo privilegio a capability, privilegi, mount scrivibili, rete e risorse, secondo il runtime concordato.
- Verificare che porte e mount esposti corrispondano al profilo; Qdrant, catalogo ed embedding non devono diventare pubblici per effetto di un default inatteso.
- Eseguire uno smoke test di avvio, workflow, manutenzione e arresto con le restrizioni abilitate; documentare le directory che richiedono scrittura.
### R12 — Segreti, backup e ripristino (SEC-12)
- Definire chi può leggere e copiare segreti, chiavi e backup; verificare permessi e redazione degli output.
- Concordare la protezione degli archivi esportati e la gestione della chiave: un archivio contenente anche la chiave di decifratura non è protetto da quella sola cifratura.
- Provare ripristino e rotazione su dati sintetici. Il modello di minaccia e l'eventuale rischio accettato devono accompagnare le istruzioni operative.
## Decisioni aperte per `grill-with-docs`
Il seguente elenco è una mappa delle decisioni, non un questionario da somministrare tutto insieme.
La skill deve chiedere, a ogni round, soltanto le decisioni i cui prerequisiti sono già risolti.
1. Quali profili devono essere supportati e quali minacce devono contenere? Gli utenti server sono reciprocamente fidati?
2. Tutti gli utenti vedono gli stessi workspace e dati? Quali operazioni sono amministrative?
3. Quale esecuzione di tool è indispensabile a Pi e quale confine di isolamento è sostenibile sul desktop e sul server?
4. Quali dati possono raggiungere modelli esterni? Quale policy vale per classificazione assente o incompleta?
5. Quale semantica e latenza di revoca servono per built-in, OIDC, SSE e lavori già in corso?
6. Quali tracce sono utili al prodotto e alla diagnosi? Con quali durata, accessi e cancellazione?
7. Quali limiti di risorse, eccezioni TLS e garanzie sui backup sono necessari nei due contesti?
8. Quali rischi bloccano un rilascio, quali sono accettabili temporaneamente e chi ne approva l'accettazione?
9. Quali confini pubblici verranno testati, con quali fixture e gate manuali? Quali migrazioni richiedono una prova di rollback?
Le risposte devono distinguere **decisione confermata**, **proposta** e **fatto da verificare**.
Non cambiare implicitamente ADR esistenti. Un nuovo ADR è opportuno soltanto per una decisione
costosa da invertire, sorprendente senza contesto e frutto di alternative reali; può bastare un
titolo e un breve paragrafo. `CONTEXT.md` resta un glossario, non il contenitore del PRD.
## Strategia di test da approvare
Preferire le interfacce pubbliche già esistenti: API HTTP/SSE con due identità, CLI e repository
di sessione nel loro percorso reale, connettori DWH, uscita verso i provider e lifecycle Docker.
Il numero di confini va mantenuto piccolo senza ridurre tutto a test unitari che non attraversano
la protezione da dimostrare.
Ogni requisito approvato deve avere un caso lecito e un caso negativo, con dati sintetici,
precondizioni esplicite e risultato osservabile. Usare fixture locali per IdP, DWH e provider;
testare l'integrazione reale soltanto in un ambiente di prova autorizzato. I valori numerici dei
limiti e delle latenze devono diventare assert misurabili prima dell'implementazione.
Confermare i confini con il maintainer prima di scrivere test. Usare `tdd` una fetta alla volta:
test rosso sul comportamento, implementazione minima, test verde. Eseguire typecheck e test
mirati durante il lavoro, le suite pertinenti al termine e `code-review` separando Standards e
Spec contro una base Git fissata. Il passaggio dei test automatici non chiude i gate manuali.
## Sequenza proposta, non ancora trasformata in ticket
1. Riconfermare i rilievi e chiudere le decisioni che determinano il modello di minaccia.
2. Approvare profili, confini di test e criteri di rilascio; aggiornare questo PRD.
3. Usare `to-spec` per la spec concordata e, con autorizzazione, pubblicarla nel tracker Gitea canonico.
4. Usare `to-tickets` per fette verticali autonomamente verificabili, ciascuna con dipendenze reali e criteri di accettazione; far approvare la scomposizione prima della pubblicazione.
5. Dare precedenza ai rischi P0 effettivi del profilo scelto, senza attendere il completamento dell'intero hardening per consegnare un controllo verificabile.
6. Dopo autorizzazione, implementare un ticket per contesto fresco nel worktree dedicato; chiudere con test, review e istruzioni operative.
Non creare automaticamente una issue per ogni riga della survey: alcune richiedono prima una
decisione, altre possono essere già risolte. Il tracker è Gitea, non il mirror GitHub. Le issue
prodotte da `to-tickets` dopo approvazione sono già pronte per `ready-for-agent`, senza ulteriore
triage; questa bozza non lo è. Dopo la pubblicazione, collegare qui la spec canonica e segnalare
che questo documento è la baseline storica, evitando due specifiche concorrenti.
## Vincoli, adozione e fuori perimetro
- Oggi sono autorizzati soltanto questi documenti e la loro navigazione. Nessuna modifica a codice, credenziali, container, server, IdP o database.
- La ripresa parte da analisi e grilling. Creazione del worktree, pubblicazione delle issue e implementazione richiedono conferma del maintainer nella sessione futura.
- Per il worktree, verificare base, nome e percorso e preservare tutte le modifiche preesistenti. Non copiare automaticamente `.env`, chiavi, dati reali o volumi operativi.
- Ogni modifica incompatibile deve descrivere diagnosi preventiva, adozione, migrazione, backup e rollback. Non migrare o cancellare sessioni reali durante i test.
- Preservare workflow a otto fasi, gate umani, resume, persistenza degli artefatti, DWH read-only e Installation Model Catalog come autorità dei modelli.
- Sono fuori perimetro: redesign generico, nuove funzionalità di prodotto non necessarie, sostituzione generalizzata dell'autenticazione, SaaS multi-tenant, pentest remoto e certificazioni di conformità.
- Il problema separato della visibilità dei modelli sul server non fa parte di questo PRD.
## Criteri di completamento del futuro intervento
Il lavoro è completo quando tutti i requisiti della spec approvata hanno evidenze e tracciamento,
i profili supportati superano i controlli automatici e i gate manuali concordati, le procedure di
adozione/ripristino sono verificate e i rischi residui hanno una decisione esplicita dell'owner.
Un rilievo storico smentito va chiuso con evidenza, non implementato per inerzia. La chiusura di
un sottoinsieme approvato non deve essere presentata come risoluzione di tutti i rilievi.
## Appendice — Punti di partenza per riconfermare le evidenze
Questi riferimenti sono indizi della survey, non prescrizioni sui file da modificare. Cercare i
simboli e i percorsi correnti alla ripresa; le righe e la distribuzione dei moduli possono cambiare.
| Rilievo | Punti di partenza nel repository |
| --- | --- |
| SEC-01 | `harness/tht/session/repository.py` (`resolve_principal`), `filesystem_repository.py`, `backend/src/config.ts`; [architettura auth](../architecture/authentication.md) |
| SEC-02 | `backend/src/pi/pi-process-manager.ts`, `provider-credentials.ts`, `harness/.pi/extensions/tht-gate.js`, repository PostgreSQL e identità di connessione |
| SEC-03 | Renderer dell'installazione, `harness/tht/config.py`, `harness/tht/db/connection.py`; [contratto DWH](../contracts/tht-dwh.md) |
| SEC-04 | `harness/tht/cli/search_cmd.py`, runtime LSH, worker descrizioni; [ADR-0011](../adr/0011-gate-source-samples-with-a-sensitive-data-flag.md) e gate in `PROJECT_STATE.md` |
| SEC-05 | `backend/src/pi/managed-config.ts`, argomenti del processo Pi, versione Pi nel lockfile e comportamento della libreria effettivamente distribuita |
| SEC-06 | `backend/src/auth/session-store.ts`, `backend/src/routes/sessions.ts`, SSE hub; [OIDC](../install/authentication-oidc.md) |
| SEC-07 | `backend/src/routes/workspaces.ts`, ammissione delle sessioni e policy dei permessi |
| SEC-08 | `harness/tht/execute/__init__.py` (`_inject_limit`, fetch dei risultati), timeout e limiti del process manager |
| SEC-09 | `backend/src/app.ts`, rate limiter del login, configurazione proxy e risposte HTTP del deploy di prova |
| SEC-10 | Lockfile backend/frontend/Pi, installazione Python nell'immagine core e inventario immagini; advisory storici su `undici`, `brace-expansion`, `protobufjs` e relativa dipendenza Pi da rivalutare |
| SEC-11 | Dockerfile e Compose in `deploy/`, confronto fra core e manutenzione, `docker inspect` con output sanitizzato solo su ambiente autorizzato |
| SEC-12 | `backend/src/workspaces/secret-store.ts`, backup/restore in `tools/tht`; [ADR-0002](../adr/0002-workspace-database-secret-references.md) |
Per le decisioni collegate consultare anche [ADR-0010](../adr/0010-allow-bounded-real-source-samples-for-description-generation.md),
[ADR-0013](../adr/0013-use-one-installation-model-catalog-with-runtime-projections.md),
gli ADR correnti sulla sensitivity e le istruzioni in `docs/agents/`.
@@ -0,0 +1,100 @@
# Riprendere il PRD di sicurezza con le skill di Pocock
**Stato:** prompt conservato per uso futuro; nessuna esecuzione programmata.
**PRD:** [Security hardening per Docker personale e server multiutente](2026-09-08-security-hardening-prd.md).
Apri una sessione nella codebase ThothII e incolla il blocco seguente. Il nome corretto della
skill è `grill-with-docs`, che combina `grilling` e `domain-modeling`. Il prompt apre la fase di
chiarimento; il passaggio a issue, worktree e implementazione resta soggetto alle conferme indicate.
```text
Riprendiamo il lavoro di sicurezza rinviato l'8 settembre 2026.
Leggi docs/plans/2026-09-08-security-hardening-prd.md. È una bozza di PRD ricavata da una
survey storica, non una spec approvata né una prova della configurazione del server remoto.
Voglio preparare interventi proporzionati per Docker su Mac/PC personale e per un server
multiutente con autenticazione built-in oppure OIDC. Il problema separato dei modelli
visibili sul server è fuori perimetro.
Usa realmente le skill di Matt Pocock: leggi le istruzioni installate, dichiarando quali
applichi. Parti da ask-matt per verificare il percorso e da grill-with-docs per il lavoro
di design; quest'ultima richiede grilling e domain-modeling. Se una skill non è disponibile,
segnalalo e concorda il fallback, senza installarla o fingere di averla eseguita.
FASE 1 — Riconferma delle evidenze, senza modificare il runtime
1. Leggi AGENTS.md, PROJECT_STATE.md, CONTEXT.md, le istruzioni docs/agents/ su dominio,
issue tracker e label, gli ADR pertinenti e il PRD. Controlla HEAD, stato del worktree
e differenze dalla baseline della survey. Preserva tutte le modifiche preesistenti.
2. Riconferma i rilievi SEC-01…SEC-12 nel codice corrente. Separa fatti verificati,
ipotesi, rischi condizionati al profilo e problemi già risolti. Non trattare i vecchi
conteggi delle dipendenze come una scansione aggiornata.
3. Usa controlli locali read-only e dati sintetici. Per un difetto da riprodurre, usa
diagnosing-bugs con un segnale ripetibile sul comportamento effettivo; una diagnosi
non autorizza ancora il fix. Confronta fatti di librerie e advisory con fonti primarie
correnti quando necessario, senza inviare codice privato o segreti ai servizi di ricerca.
4. Non accedere o intervenire su server, IdP, DWH o provider reali senza aver concordato
target e operazioni. Non mostrare API key, cookie, password o campioni di dati reali.
Esito della fase: una matrice aggiornata che conserva gli ID dei rilievi, con evidenza,
profilo interessato e stato. Un fatto ancora non verificabile resta esplicitamente aperto.
FASE 2 — grill-with-docs, con me presente
5. Costruisci l'albero delle decisioni. Parti da profili di deploy, fiducia fra utenti e
condivisione dei dati; poi affronta isolamento di Pi, policy dei valori sensibili,
revoca, retention e limiti seguendo le dipendenze effettive.
6. A ogni round presenta soltanto le domande attualmente sbloccate, numerate, con la tua
raccomandazione e i trade-off. Attendi le mie risposte prima di assumere le decisioni
successive. Cerca autonomamente i fatti ricavabili dal repository; usa agenti di
ricerca mirati quando previsto dalla skill, senza delegare a loro le mie decisioni.
7. Aggiorna il PRD distinguendo proposte e decisioni confermate. Aggiorna CONTEXT.md solo
per termini realmente risolti. Proponi ADR soltanto per scelte difficili da invertire,
sorprendenti senza contesto e fondate su alternative reali: basta il formato minimo.
8. Concorda requisiti, priorità, rischi accettati e criteri misurabili, inclusi i tempi
di revoca e i limiti di risorse. Conferma con me i confini pubblici dei test prima
di scriverli. Mantieni espliciti i gate manuali già presenti in PROJECT_STATE.md.
Esito della fase: nessuna decisione bloccante lasciata implicitamente all'agente;
riepilogo e mia conferma della comprensione condivisa. Fino a quella conferma rimani
su analisi e documentazione: nessun cambiamento applicativo o di deployment.
FASE 3 — Spec e ticket, soltanto dopo mia conferma
9. Usa to-spec per sintetizzare le decisioni già prese, senza riaprire arbitrariamente
l'intervista. Chiedimi conferma della pubblicazione prima di creare la spec nel
tracker canonico Gitea indicato in docs/agents/issue-tracker.md, non nel mirror GitHub.
Collega la spec canonica dal PRD e rendi chiaro quale documento è la fonte aggiornata.
10. Usa to-tickets per proporre fette verticali verificabili autonomamente, dimensionate
per un contesto fresco. Collega ogni ticket ai requisiti e ai rilievi pertinenti,
indica i veri blocker e includi criteri positivi e negativi. Fai approvare granularità
e dipendenze prima di pubblicare. Solo i ticket approvati e completi ricevono
ready-for-agent; non rimetterli in triage e non chiudere automaticamente la spec padre.
11. Se una decisione richiede una prova eseguibile, proponi un prototype limitato a quella
domanda prima di fissare la spec. Usa wayfinder solo se il lavoro risulta realmente
troppo ampio e incerto per essere chiarito con grill-with-docs.
Esito della fase: spec approvata e ticket autosufficienti con dipendenze risolte o esplicite.
Chiedimi se autorizzo il primo ticket: l'approvazione del design non avvia da sola il codice.
FASE 4 — Implementazione futura autorizzata
12. Prima di modificare codice, concorda e crea un worktree dedicato, verificando percorso,
branch e commit base. Non riusare una directory occupata, non alterare il worktree
originario e non copiare automaticamente segreti o dati operativi. Assicurati che
PRD e prompt siano disponibili nel worktree attraverso un passaggio esplicito.
13. Esegui implement su un ticket sbloccato per volta, in un contesto fresco. Segui tdd
ai confini concordati: un test rosso sul comportamento, implementazione minima,
test verde. Esegui typecheck e test mirati durante il lavoro e le suite pertinenti
al termine; usa fixture locali per IdP, DWH e provider.
14. Esegui code-review sui due assi Standards e Spec, usando i due agenti previsti dalla
skill e una base Git fissata. Assicurati che il diff esaminato includa tutto il lavoro
del ticket, anche se ancora non committato; un diff vuoto non è una review superata.
Risolvi i rilievi e verifica di nuovo. Commit soltanto del lavoro pertinente nel
worktree autorizzato; push, merge e deploy richiedono un'autorizzazione distinta.
15. Consegna evidenze dei test, istruzioni di adozione e rollback, gate manuali pendenti
e rischi residui. Non dichiarare chiuso il PRD intero se è concluso soltanto un ticket
o se resta un'accettazione dell'owner.
Inizia dalla Fase 1, poi proponimi il primo round di grill-with-docs.
```
@@ -0,0 +1,112 @@
# X1 — validation of session archive corrections
Date: 2026-09-09. The joint Memory/Evidence repair increment is implemented.
The authoritative contract is [Session archive corrections](../contracts/archive-repair.md).
## Delivered behavior
The session gate shows complete before/after content for specific alternatives targeting
Memory or Evidence. The reviewer chooses one correction or rejects all proposals as
inadequate and requests reformulation. The resulting receipt survives interruption;
saved content and index activation are reported separately. Pending activation offers
retry of the same chosen operation. A subsequent curator change blocks replay.
Application requires an administrator in the harness and the responding browser
principal's archive-management permission. Cross-principal runtime responses cannot
misattribute the correction. A non-administrator can decline or continue the current
question without modifying shared archives. The gate does not advance a workflow phase.
Memory and Evidence remain separate domains. The integration coordinator reuses their
canonical persistence and activation operations. A session Evidence correction requires
a consolidated archive, preserves source lineage, and cannot publish unrelated external
edits. Existing administration, source import, dependency cleanup and final Memory review
remain available. No automatic Git commit or push was added.
## Verification
- Harness regression: **1,264 passed**, one skipped, five deselected. All **nine**
portable-path checks passed separately without `THT_HOME`. Python lint passed on changed modules.
- Backend: **1,366 passed**, 40 skipped. Tests include actual session-response routes
for both target archives, unauthorized response, malformed choice and runtime ownership.
- Frontend: complete suite **645 passed**; the final display adjustment passed all four
focused widget tests. Backend and frontend TypeScript checks passed.
- Pi extension: **199 passed**, including closed human choices, rejection, failure/retry,
forged selections and the updated public tool schema. The modular skill projection is
byte-identical to its updated approved template.
- PostgreSQL/Qdrant integration traverses the actual Python CLI for preparation,
application and recovery inspection. Corrected Memory and Evidence are retrieved from
real indexes, and Evidence activation preserves the Memory card. Session loading is a
controlled fixture and embeddings are deterministic; this is not an LLM quality test.
- Failure tests cover both targets, index outage, replay, later edits, workspace/session
isolation, changed session context, rejection, non-admin writes, and interruption after
the Evidence file write but before its saved receipt.
- Playwright desktop/mobile: **one passed**. The real widget renders both alternatives,
accepts an Evidence choice, displays pending activation and allows retry to active.
No page errors or mobile horizontal overflow. Screenshots are
`/private/tmp/thothii-x1-repair-desktop.png` and `/private/tmp/thothii-x1-repair-mobile.png`.
This browser fixture controls operation outcomes; persistent behavior is tested above.
- Strict MkDocs build and `git diff --check` passed.
## Local installation and reviewer acceptance
Core and frontend images were rebuilt from this worktree using the existing local
preview launcher. Migration `004_archive_repairs.sql` was applied to the existing
installation catalog. The new gate is available to session workflows; it is not an
always-visible administration panel. Existing PSD archive content was not changed by
the synthetic validation cases.
All five local services are healthy at `http://127.0.0.1:8080/`.
The technical increments and their planned checks are complete. The end-user acceptance
check remains a real session containing a meaningful domain conflict, with the reviewer
evaluating the proposed correction. Automated browser validation uses temporary accounts
and data, not the user's authenticated PSD session. Source import retains its E3
validation boundaries; no broader model-quality benchmark was added.
## Follow-up acceptance: configured model
The opt-in `test_real_model_proposes_a_reviewable_persistent_archive_correction`
passed with the installation's **zai/glm-5.3** model. Synthetic Memory asserted an
order-ID-only join; synthetic Evidence required the financial year too. The model
returned two schema-valid, specific alternatives with complete content and the exact
target revisions. The test reviewer selected Memory, persisted the correction through
the real coordinator and PostgreSQL, and retrieved the updated rule. Evidence stayed
unchanged. This test uses deterministic vectors and the configured completion helper;
it does not claim a full autonomous Pi session or human acceptance of PSD semantics.
The run log is `/private/tmp/x1-acceptance-model.log`. Reproduce with
`THT_MEMORY_L2_INSTALLATION=<installation.yaml>` and `THT_MEMORY_L2_CORE=<core-container>`
using `pytest -q -s -m l2 tests/memory/test_administration.py -k real_model_proposes`.
Credentials are resolved inside core and are not returned to the test runner.
## Follow-up acceptance: both administration pages
The opt-in `frontend/e2e/memory-real.spec.ts` passed through real authentication,
Fastify, ThtRunner, Python, isolated PostgreSQL and Qdrant. It verifies:
- Database management, Memory management and Evidence management appear as peers in
that order, with no active core session or DWH binding required.
- Memory creation, editing, persistence across backend restart, deletion and absence
from subsequent recall.
- Canonical Evidence remains intact after the Memory deletion. Its full rule is read
through the real Evidence administration worker; content filtering finds it and an
unmatched filter produces the empty state.
- Requests for an unregistered workspace return 404 for both archives.
- Desktop and mobile Evidence views render without horizontal document overflow.
On phones, both archive pages have at least 380px of usable width at a 390px viewport.
Navigation opens in the shared accessible dialog, closes with Escape or archive selection,
and returns focus to the trigger after Escape.
The temporary PostgreSQL readiness probe now waits for TCP, avoiding the image's
socket-only initialization server. The browser waits for Memory refresh to finish
before leaving its page, matching the existing navigation guard. Visual inspection
also exposed a real mobile layout issue: the fixed sidebar left only 134px for the
Evidence page. `ArchiveNavigation` now moves that sidebar into the shared dialog below
768px on Memory/Evidence pages. Desktop behavior is unchanged. The frontend image
was rebuilt for the local preview.
Run log: `/private/tmp/x1-acceptance-browser7.log` (**one passed**).
Screenshots: `/private/tmp/thothii-acceptance-evidence-desktop.png` and
`/private/tmp/thothii-acceptance-evidence-mobile.png`. Reproduce with
`THT_MEMORY_BROWSER_E2E=1 npx playwright test e2e/memory-real.spec.ts` from `frontend/`.
The fixture removes its temporary containers, accounts and checkout on completion.
The TypeScript check, Python lint, strict documentation build and diff check also pass.
@@ -0,0 +1,67 @@
# Evidence E1 — validation
Date: 2026-09-09. Scope: editable Curated Evidence v4 and the persistent local archive.
## Implemented behavior
- Parser, renderer, authoring output and normalization share the existing typed payloads.
Visible Markdown edits determine content for all eight kinds. Legacy v1–v3 conversion
is explicit and lossless, with errors for content that cannot be represented exactly.
- Manual declarations record the curator. A correction preserves the original document
as lineage, separately from the current declaration. No source hash is needed to
create a manual file.
- The local archive records baselines, immutable candidates, active revisions and
deletion/source suppression metadata. Unresolved review items and invalid edits block
consolidation. Missing archive directories are availability failures, not deletions.
- Activation failures preserve the previous active revision. Interrupted normalization
replays only unchanged input bytes; later operator edits survive recovery.
- Revision-checked correction methods reject stale workflow updates. Legacy preparation
and resolution cannot overwrite an initialized local archive; explicit import/refresh
integration is deferred to E3.
## Verification
The final harness suite excluding opt-in L0/L2 and portable-layout cases passed with
**1,180 tests** (58 deselected). All **9 portable-layout tests** passed separately with
`THT_HOME` unset. The dedicated real-Qdrant integration test passed, including the
optional 35-unit PSD probe. Ruff passed on the changed Evidence implementation and
tests, and the strict documentation build succeeded. No frontend or backend TypeScript
changes are part of E1.
The integration test uses an isolated Qdrant 1.18.2 container, the actual corpus
pipeline, semantic chunking, vector adapter and active Evidence searcher. Deterministic
three-dimensional embeddings isolate file/content correctness from model behavior.
It verifies that raw edits do not change recall, consolidation updates recalled content
and curator identity, a blocked candidate preserves prior recall, and deletions remove
recall. Existing schema and Memory records survive each operation.
All **35 PSD units** were copied from the owner's workspace into
`/private/tmp/thothii-e1-psd.bsW4cp`. Deterministic conversion preserved every ID, payload,
scope, provenance and review item. There were no unresolved review items. The optional
integration probe then indexed all 35 converted units and compared their complete ID
set to the original. It uses PSD's actual `max_chunk_chars: 5000`; a preliminary probe
at 4000 correctly blocked an oversized atomic unit.
Reproduce the isolated real-corpus probe after creating a converted workspace copy:
```sh
cd harness
THT_E1_PSD_COPY=/absolute/path/to/converted-copy \
.venv/bin/pytest -q -s tests/test_evidence_editable_integration.py
```
The environment variable is optional. Ordinary CI uses only synthetic Evidence. No
source refresh, external document download, DWH call or model request is involved.
## Delivery boundary
E1 is a core/library increment. E2 must add the installed manual consolidation command,
connect runtime source selection to the active local snapshot, and build administrative
list/filter/detail with real persistent host paths and manual Git instructions. E3
adds source acquisition and explicit refresh/conflict handling. X1 later wires deliberate
joint Memory/Evidence corrections into review gates.
The actual PSD Evidence checkout was not converted. The live Docker preview at
`http://127.0.0.1:8080` remains the previously deployed M3 stack, with no new Evidence
administration page. The corpus conversion and reindexing described here used copies
and disposable test resources.
@@ -0,0 +1,86 @@
# Evidence E2 — validation
Date: 2026-09-09. E2 is implemented locally and installed on the existing Docker preview.
E3 source imports/refresh and X1 deliberate Memory/Evidence workflow corrections remain open.
## Delivered behavior
- Independent **Administration → Evidence management**, after Memory, protected by
`evidence.manage`: complete typed content, provenance and original excerpts, review items,
pagination, search, kind/purpose/status and scope/source filters, sort, and refresh.
- Working-file states distinguish active, modified, new, removed, invalid, legacy and review
required. Detail shows the actual configured host path with copy controls. Instructions
cover external editing, all eight Markdown templates, consolidation and manual Git.
- Installed `tht workspace evidence consolidate --workspace <id> [--json]` uses a closed
maintenance envelope. First use converts legacy units. Validation, immutable candidates,
activation and retry run through the existing corpus pipeline without a DWH scan or Git.
- Runtime and ordinary preprocessing consume the active local snapshot. Unconsolidated
edits remain excluded. Catalog/Schema readiness is not advanced by this operation.
Clear preserves curated files, archive metadata and Memory; full preprocessing must
recreate the missing Reference/Schema derivations afterward.
- Immutable runtime lease filenames now identify rendered bytes as well as logical input
identity. This fixes upgrades colliding with old runtime files without changing Catalog
fingerprints or removing the checks against tampered files.
## Automated checks
The complete backend suite passed: **1,355 tests**, 40 skipped. The complete frontend
suite passed: **639 tests**. Both TypeScript checks passed. Native Go workspace operation
and CLI tests passed, including rejection of arbitrary consolidation flags. Ruff passed
for changed Python implementation and test files.
The harness run passed **1,248 tests**, with one skipped and five deselected. Its three
portable-path tests failed because that run deliberately set `THT_HOME` to the test
runtime; rerunning the portable tests with `THT_HOME` unset passed. The final focused
administration/path suite passed all 18 tests, including actionable migration errors and invalid
consolidation combinations rejected before cleanup or indexing.
The real-Qdrant integration test exercised the actual harness consolidation CLI with
deterministic embeddings: all 35 PSD units converted and indexed with stable identities;
active-only source selection; separate Schema and Memory canaries; Clear and rebuild
from the retained snapshot. Unit tests cover validation, saved-but-unindexed failure,
retry, browsing/filtering, no automatic Git, and no Catalog mutation from consolidation.
A temporary Git repository and bare local remote exercise the documented manual sequence:
edit, add and remove files, consolidate, inspect, stage the complete Evidence tree,
commit, push and clone. The clone retains changed content, additions, deletions, managed
metadata and an accessible active snapshot. No remote user repository was pushed.
## Installed preview
The existing Compose project is `thothii-18998cca7b0a`, at `http://127.0.0.1:8080`.
The persistent editable checkout is:
```text
/Users/mp/projects/ThothII/deploy/psd/evidence-registry/repo/psd-clinical/evidence
```
The original registry checkout was copied from its retained Docker volume. The original
author repository was not changed. Core and maintenance share a nested host bind for
`repo`; registry state/snapshots and all other existing data volumes were retained.
The installation descriptor includes the existing workspace bindings and the new
`evidence-host.yaml` override. The previous descriptor and native binary are backed up
at `/private/tmp/thothii-installation-before-e2.yaml` and `/private/tmp/tht-before-e2`.
The real installed command succeeded with **35 documents, 35 chunks, 35 changed, zero
removed**, using the configured embedding service and Qdrant. A second run succeeded
with **35 unchanged, zero changed**. Reading the actual archive from core returned
35 active units and no file errors. All five long-running services are healthy.
Only the Evidence stage ran. The strict documentation build and `git diff --check`
also passed. The stack launcher is `bash /private/tmp/thothii-memory-preview.sh`; keep its
worktree image-build override until this branch is integrated into the main checkout.
Browser verification reached the local login page. The saved administrator password
does not match the current account hash, so the authenticated visual check remains
manual. No account or password was modified. React interaction tests cover navigation,
detail, host paths, templates, filtering, pending activation and invalid files.
## Boundaries
There is no web content editor, watcher, automatic commit/push, or implicit source refresh.
The API exposes administration reads and consolidation; the archive's revision-checked
save/remove operations remain available for the later explicit workflow corrections.
These gates are not claimed as implemented by E2. Initialized local archives retain
structural/review checks but bypass the legacy fixed retrieval-evaluation fixture so
its old expected IDs cannot veto deliberate deletions. A general retrieval benchmark
is outside the agreed scope.
@@ -0,0 +1,100 @@
# Evidence E3 — validation
Date: 2026-09-09. Explicit source import/refresh and decisions are implemented. X1,
the integration of deliberate Memory/Evidence corrections into workflow gates, remains next.
## Delivered behavior
The independent Evidence page now offers **Sources and imports**. An operator copies
a specialist's draft into `evidence/incoming/`, then explicitly imports/refreshes.
Original local Markdown and configured HTTP/S3 sources use existing read-only adapters.
Acquisition retains raw bytes, source identity and versioned normalized documents.
The existing Pi authoring refiner prepares typed, editable v4 proposals.
Unchanged hashes skip refinement. All source acquisitions/refinements must succeed
before saving a new set of comparisons. Missing sources are recorded as unavailable,
never interpreted as permission to delete. No runtime lookup, ordinary consolidation
or preprocessing triggers remote refresh once the local archive is initialized.
The administrator sees current and proposed units, scope, content, excerpts, review
items and explicit retirement IDs. **Keep local Evidence** records the retained wording
as a manual declaration with original lineage. **Use proposed Evidence** adopts the
proposal and its source version. Both save and activate through the existing archive
and corpus pipeline; review items block adoption. Comparisons use optimistic checks
on affected file bytes. Interrupted decisions have a durable replay journal and retry
without reacquisition, while intervening external edits are preserved and reported.
Deleted IDs remain reserved. New model-generated identities from sources with curated
deletions are also conservatively suppressed; surviving IDs can still receive reviewed
updates. Deliberate new knowledge can be authored as a manual file. This mechanical
protection does not depend on the model detecting semantic duplication or contradictions.
Installed commands are `workspace evidence refresh` and `workspace evidence decide`,
alongside E2 consolidation. Decision envelopes carry a source identity, comparison
revision and keep/replace choice. Extra URLs, arbitrary paths, forged actors and unknown
fields are rejected at the public API/CLI boundary. HTTP requests bind the authenticated
curator. Source operations do not mutate Catalog readiness or run DWH/schema stages.
The Python source worker is internal; the workflow CLI's visible surface is preserved.
## Checks
- Complete backend suite: **1,359 passed**, 40 skipped. Complete frontend suite:
**641 passed**. Both TypeScript checks passed; native Go CLI/workspace tests passed.
- Harness regression run: **1,256 passed**, one skipped and five deselected, with
portable-path tests run separately without `THT_HOME`. All **24 focused import,
CLI-surface and portable-path checks** passed. These
cover import, unchanged refresh, access failure, missing source, manual correction,
keep/replace, deletion suppression, stale comparisons, failure/retry and interrupted
journal writes. Ruff passed on the changed Python implementation and tests.
- The real-Qdrant test traverses the actual harness source CLI with deterministic
refinement/embedding boundaries: import is absent from recall before a decision,
accepted content becomes searchable, refreshed proposals preserve active manual
corrections, replacement removes the former text, deletion remains absent after
another refresh, and unrelated Schema/Memory canaries survive.
- Source contract fixtures cover controlled HTTP and S3 identities, exact acquired
bytes, and acquisition call counts. Existing adapter tests retain transport/egress
coverage. The test does not claim to exercise a live S3 account.
- React interaction tests verify explicit refresh, comparison content, exact decisions,
saved-decision retry and failure feedback. Route tests cover admin authorization,
workspace isolation, strict inputs and principal attribution. Service tests verify
the trusted config file descriptor and absence of Catalog mutation.
## Local preview
Core/frontend were rebuilt for the existing `thothii-18998cca7b0a` stack. Its persistent
archive and data volumes are retained. The native `/usr/local/bin/tht` was updated;
the previous executable is at `/private/tmp/tht-before-e3`.
The installed refresh command ran against `psd-clinical` successfully: **35 unchanged
sources, zero changed, zero pending comparisons**. All 35 source hashes matched their
existing units, so this probe required no refinement and changed no active Evidence.
Source registry metadata was saved locally; no Git commit or push was performed.
A separate synthetic draft was passed to the configured Pi/model inside core. It
produced one domain proposal with one review item, which was not activated. The probe
exposed a deployment issue: Python wheel modules and Pi skills live in different
directories. The refiner now resolves resources through `THT_HARNESS_DIR`, with the
source-tree location as its development fallback; a regression test covers this layout.
The corrected installed worker was then exercised end to end in a temporary workspace
inside core, using the real configured Pi/model and a synthetic `incoming/orders.md`.
It returned success, one changed source, one comparison and one proposal with a review
item. The active snapshot remained absent. The temporary directory was removed on exit;
the probe did not open the DWH or activate an index. Strict docs build and
`git diff --check` also passed.
The in-app browser still showed the login page with the prior credential error.
Authenticated visual verification remains manual; no credentials were reset or retried.
## Operational instructions and boundaries
See [Import drafts and refresh sources](../contracts/curated-evidence-v4.md#import-drafts-and-refresh-sources)
for commands, local paths, review, retry and backup/Git requirements. Preserve the
complete Evidence tree, including source comparisons, journals and acquired versions.
Local activation and transfer to the remote Git repository remain separate operator steps.
No web content editor, automatic Git, background watcher, new job queue, general
retrieval benchmark or automatic source merge was introduced. Review/refinement is
sequential and bounded by per-source limits plus 200 documents/100 MiB per refresh.
New changed-source decisions and failure scenarios use isolated test data; live PSD
curated content was kept unchanged. X1 is not included in this increment.
@@ -0,0 +1,96 @@
# M2 — Ricerca ibrida e collegamenti
Data: 2026-09-09. Implementazione locale dell'incremento M2 del
[piano Memory approvato](2026-09-08-memory-management.md#piano-esecutivo).
## Risultato
La ricerca Memory ed exemplar usa embedding dense e BM25 in Qdrant. I filtri sono
applicati in entrambi i rami prima della selezione dei candidati. Il core espande
i collegamenti uscenti, deduplica, limita cicli e visite, riordina insieme i risultati
e restituisce il contenuto corrente verificato in PostgreSQL. Non usa un database
a grafo né una chiamata LLM per il riordinamento.
Il contesto fisico distingue database, schema, tabella e colonna e richiede che
corrispondano alla stessa dipendenza strutturata. Le card senza dipendenze valgono
per il workspace; un riferimento a un antenato fisico si applica ai suoi discendenti.
Ambito descrittivo e concetti possono restringere ulteriormente la ricerca tramite
`--filters`. La CLI impedisce di sostituire database/schema del runtime. Il dettaglio
dei limiti e della formula di ranking è nel [contratto operativo](../gestione-memory.md#hybrid-recall-and-links).
L'incremento conserva i confini dei gate correnti: F2 riceve chiarimenti di dominio,
gli exemplar restano consultativi. Un collegamento non autorizza a consumare una
famiglia diversa o una card fuori ambito. Il riepilogo finale, i nuovi gate e la
pulizia delle dipendenze dopo sincronizzazione fisica appartengono a M3.
## Transizione e recupero
La migrazione versionata `002_hybrid_projection.sql` aggiunge il formato delle
proiezioni. Le card M1 restano autorevoli e consultabili; le loro proiezioni dense
risultano pendenti e non possono alimentare il recall. Un retry esplicito o
`tht memory index -c <runtime.yaml>` costruisce dense e BM25 dal contenuto corrente.
Il formato della proiezione e la revisione della card sono verificati prima dell'uso.
Il salvataggio può aggiungere il vettore sparse mancante nella sola collezione
Memory. Non sostituisce configurazioni incompatibili e non modifica Reference.
Il rebuild esplicito ricrea anche una collezione Memory assente; il test elimina
la collezione, ricostruisce da PostgreSQL e verifica il ritorno della sola card conservata.
Fallimenti lasciano il lavoro di propagazione persistito e recuperabile. Nessuna
importazione da JSONL, sessioni storiche o vecchi payload Qdrant.
## Verifiche
| Controllo | Esito |
| --- | --- |
| Suite harness senza L0/L2, escluso il file dei percorsi portabili | 1.152 test passati nell'esecuzione finale. |
| Suite mirata Memory, adapter e CLI, con embedding reale | 78 test passati. |
| Verifica aggiuntiva del rebuild con collezione assente e adapter | 54 passati; il solo test del modello reale era escluso in questa riesecuzione. |
| API Fastify Memory | 13 test passati, compresa propagazione della lingua del workspace. |
| Browser amministrativo integrato | Passato: creazione, modifica, riavvio, rilettura, cancellazione e verifica del recall. |
| Wheel e casi CLI | 10 test passati; il wheel include entrambe le migrazioni Memory. |
| Build e controlli statici | Build/typecheck backend, Ruff sui file Python interessati e build documentale strict passati. |
- Test deterministici: collegamenti necessari, contenuto corrente, duplicati,
cicli, profondità e limiti, card mancanti o pendenti, rifiuti già registrati,
famiglie e ambiti esclusi, dipendenze omonime e filtri CLI vincolati al runtime.
- Adapter: stesso filtro nei prefetch dense/BM25 per Memory ed exemplar; restano
coperti i contratti Evidence esistenti.
- PostgreSQL/Qdrant: salvataggio, modifica, cambio famiglia, cancellazione,
ricostruzione, riferimento separato, isolamento, RLS, errori, retry e transizione
delle proiezioni M1 al formato ibrido.
- Recupero effettivo: client Ollama di produzione con il modello configurato
`qwen3-embedding:0.6b`, dimensione 1024, e Qdrant dell'immagine fissata in Compose.
La domanda sulla chiave commessa fra esercizi recupera la regola attesa e la
granularità collegata, escludendo un altro database, un altro ambito e dipendenze
che corrispondono soltanto combinando riferimenti distinti. Verifica separatamente
dense, BM25 e fusione, poi recall, cancellazione e rebuild.
Il test effettivo avvia un processo Ollama separato, montando il volume del modello
installato in sola lettura. PostgreSQL e Qdrant sono container temporanei dedicati,
eliminati a fine test. Non usa ID di risultati predisposti. Il browser amministrativo
usa invece embedding deterministici: verifica il collegamento fra UI, autenticazione,
Fastify, ThtRunner e persistenza, senza essere una misura di qualità del recupero.
Non è una valutazione generale della qualità semantica su un corpus di produzione;
verifica i casi di recupero richiesti da M2, con il percorso reale configurato.
## Riproduzione
Dalla directory `harness`, con Docker disponibile:
```sh
THT_HOME=/private/tmp/thothii-m2-test-home \
THT_MEMORY_TEST_OLLAMA_VOLUME=<volume-modelli-installazione> \
THT_MEMORY_TEST_MODEL=qwen3-embedding:0.6b \
THT_MEMORY_TEST_DIMENSIONS=1024 \
.venv/bin/pytest -q tests/memory/test_administration.py \
tests/memory/test_retrieval.py tests/memory/test_recall.py \
tests/test_qdrant_vector_store.py tests/test_solved_search_cli.py
```
Senza il volume esplicito, il solo test con modello reale viene escluso; gli altri
test restano eseguibili. Il volume deve contenere il modello indicato. Le immagini
Qdrant e Ollama del test sono lette da `compose.yaml`.
Consegna locale: non sono stati eseguiti deploy, migrazioni delle installazioni
attive o aggiornamenti remoti dell'issue tracker.
@@ -0,0 +1,78 @@
# Memory M3 — validation
Implemented on 2026-09-09 in the rapid-harbor worktree.
## Delivered behavior
- F8 presents an editable summary of proposed additions, explicit updates and links.
Only selected content is saved, including the optional solved-question exemplar.
- Proposals reference effective approved decisions. Exact existing content is reused.
Concurrent edits invalidate an update; manual identities and origins are preserved.
- Selected cards and links commit atomically. Durable receipts recover repeat delivery
and the gap before the session review marker. Finalization does not add Memory.
- F4/F6/F7 retrieve SQL rules and explained errors for the existing approval gates.
Retrieval is consultative and does not write an approval decision.
- Successful Catalog physical sync deletes cards with matching removed dependencies.
The same Catalog transaction marks pending Memory cleanup. Retry keeps the original
removals and does not rescan; deletion receipts and projection tombstones survive restarts.
- Migration 003 adds minimal review and physical-cleanup receipts.
## Executed checks
| Check | Result |
| --- | --- |
| Harness deterministic suite, excluding portable-path environment cases | 1,152 passed |
| Portable-path suite without the temporary THT_HOME override | 9 passed |
| Memory service/retrieval with isolated PostgreSQL and Qdrant | 41 passed, 1 optional real-embedding case skipped, 1 L2 case excluded |
| Pi gate suite | 195 passed |
| Backend full suite | 1,346 passed; one auth timing test exceeded 5 seconds under concurrent load |
| Isolated auth and Catalog route rerun | All 40 passed, including the timed-out case |
| Catalog PostgreSQL integration after adding atomic cleanup-marker coverage | All 5 passed |
| Frontend full suite | 635 passed |
| Chromium summary review, desktop and 390px mobile | Passed; no page errors or horizontal overflow |
| Configured real GLM 5.3 generation | Passed on synthetic PostgreSQL data |
| Backend/frontend production builds, modified Python lint, strict docs build | Passed |
The existing local Docker preview was rebuilt from this worktree, migration 003
was applied, and core/frontend were recreated with the existing persistent volumes.
The preview remains at `http://127.0.0.1:8080`.
The browser check uses the production widget in an isolated Vite fixture. It edits
the rule, declines the exemplar, submits only the selected card and checks responsive
layout. Gate tests separately verify request ordering through the production Pi
composition root; service and Catalog tests use real PostgreSQL. This is not a claim
of an automated complete live Pi conversation.
The L2 case retrieves an approved SQL rule, excludes a card bound to another database,
and asks the configured GLM 5.3 model to generate a query. Order IDs repeat between
financial years; the correct composite join returns 120 on the synthetic fixture.
The generated SQL is validated and executed in a read-only PostgreSQL transaction.
No real DWH rows are sent. Embeddings in this case are deterministic; the real
embedding/hybrid retrieval evidence remains documented in M2.
## Reproduction
From the harness:
```sh
THT_HOME=/private/tmp/thothii-m1-harness-home .venv/bin/pytest -q -m 'not l0 and not l2' --ignore=tests/test_portable_paths.py
.venv/bin/pytest -q tests/test_portable_paths.py
THT_HOME=/private/tmp/thothii-m1-harness-home .venv/bin/pytest -q tests/memory/test_administration.py tests/memory/test_retrieval.py -m 'not l2'
npm test
```
The optional generation case requires an installation YAML path and its running
core container. It resolves the model credential inside core without printing it:
```sh
THT_MEMORY_L2_INSTALLATION=<installation.yaml> THT_MEMORY_L2_CORE=<core-container> \
.venv/bin/pytest -q -s -m l2 tests/memory/test_administration.py -k real_model
```
From frontend: `npx playwright test e2e/memory-review.spec.ts`.
Screenshots are written to `/private/tmp/thothii-m3-summary-desktop.png` and
`/private/tmp/thothii-m3-summary-mobile.png`.
Evidence authoring and the joint X1 persistent Memory/Evidence conflict repair remain
outside M3. This increment does not infer knowledge from unexplained failures or
promise general improvements in SQL-generation accuracy.
@@ -0,0 +1,84 @@
# Unified Administration Pages
Status: revised direction A accepted and implemented locally (2026-09-12).
The owner selected the latest collapsible context shelf A after restoration of
the original Core. Core and session management must remain functionally unchanged;
the five Administration pages and global context are the implementation scope.
Prototype history remains in `frontend/prototypes/`, including
`administration-review/README.md` and `context-shelf/README.md`.
Final integration remains on the server.
See [implementation and acceptance evidence](../reports/2026-09-12-context-shelf-a-implementation.md).
## Problem Statement
Workspace and Pi open over the work area while Database, Memory and Evidence have
different page structures. Administrators need five predictable, independently
accessible pages that fit beside the Omics Portal sidebar and below its red header.
## Solution
Use the chosen A / Workbench direction for all five Administration Pages: a warm
page heading, restrained red actions, readable serif titles, sans-serif controls,
compact identity/status information, and an index/detail work area appropriate to
each domain. Keep B / Inspector and C / Operations Deck as recoverable prototypes.
Navigation remains on the right, collapsing within the available application width.
## User Stories
1. As an administrator, I want every management entry to open a full page, so that I can use the entire work area.
2. As an administrator, I want the five pages to share hierarchy and typography, so that actions and current context are predictable.
3. As an administrator, I want to refresh or bookmark a management page, so that I can return directly to it.
4. As an administrator, I want Back and Forward to restore navigation, so that browser controls behave normally.
5. As an administrator, I want unsaved edits protected during navigation, so that leaving a page does not silently discard work.
6. As an administrator, I want existing permissions enforced on direct links, so that a URL cannot bypass access controls.
7. As a reviewer, I want my active session retained while visiting Administration, so that navigation does not restart or stop Pi.
8. As a workspace operator, I want to select a workspace and inspect its source, runtime requirements and validation, so that I know which environment I am preparing.
9. As a workspace operator, I want preprocessing beside that workspace's readiness, so that the operation's target is explicit.
10. As a workspace operator, I want catalog revision and preprocessing diagnostics, so that I can understand missing prerequisites and stale derived data.
11. As a database operator, I want configuration, synchronization, descriptions and sensitivity in Database management, so that ownership of catalog changes remains clear.
12. As a database operator, I want a link to the related workspace's preprocessing status, so that I can complete preparation after catalog changes.
13. As a Memory curator, I want existing filtering, CRUD, links and index recovery inside the shared workbench, so that the redesign preserves my workflows.
14. As an Evidence curator, I want browsing, source review and file-maintenance instructions inside the shared workbench, so that external Markdown editing remains the authoring workflow.
15. As a Pi operator, I want runtime status, catalog defaults, models and diagnostics in a page, so that I can inspect the installation without a management popup.
16. As a portal user, I want layout based on the available container width, so that a desktop viewport with a wide portal sidebar still works.
17. As a keyboard or mobile user, I want accessible navigation and non-overlapping controls, so that all five pages remain usable at narrow widths and zoom.
18. As a product owner, I want B and C preserved, so that an individual page can adopt another design later.
## Implementation Decisions
- Five peer Administration Surfaces, independent of active session existence.
- One collapsible top shelf A selects workspace and canonical interaction model for Core and all Administration pages. No duplicate operational selectors inside those pages or the Core composer.
- Each explicit choice is remembered independently per browser origin, application mount and authenticated principal. Absent an explicit choice, use the installation default. Removed/unavailable choices require explicit correction, never silent substitution.
- Neither Core nor Administration activities are enabled until both choices resolve to available catalog entries. Background refresh errors retain already validated context and mounted drafts.
- Workspace/model changes are locked during Core or Administration operations. Navigation itself does not cancel those operations: visited Administration pages and the original Core stay mounted.
- Resume returns the session's pinned workspace in its lifecycle response and adopts that workspace without replacing the global model. Older responses can fall back to the session manifest.
- Preserve the original eight-phase workflow, left activity log, gate widgets, composer, My sessions/All sessions tabs, groups, archive and session lifecycle. A navigation drawer remains reachable when the log is open or the available width is narrow.
- Shared Workbench page/header/layout primitives; data ownership and mutation APIs remain domain-specific.
- Namespaced `thoth_route=administration/<surface>` query routing, preserving host path, other query parameters, fragment and history state. Optional `thoth_workspace` carries only a stable workspace identity for cross-links.
- Unsaved Administration changes block in-app navigation, browser Back/Forward, and context changes. Save successfully or explicitly cancel inside the editor before leaving; navigation does not offer automatic discard. Reload gets native before-unload protection. Busy state locks context selection, not read-only cross-page navigation.
- Workspace owns readiness and full preprocessing. Database owns binding, physical schema synchronization, descriptions and sensitivity. Database status links to workspace preparation; it does not duplicate the run action.
- Preprocessing permission remains the existing backend permission. This UI uses the already unified Installation Model Catalog (`defaults.interaction`) and does not introduce another model authority.
- A shared responsive frame uses available container width, scoped CSS and bounded scrolling. The portal remains the owner of its header and left sidebar; Thoth does not duplicate them. The host can set `--thoth-app-height` to its available height below the header.
- Existing catalog grids remain appropriate for tabular data; form content is inline in the work area. Short confirmations and secondary operation/history panels may remain dialogs/panels.
- English interface labels; persisted document content retains the workspace language.
- A is the production direction. The three read-only prototypes and their launch script remain available and are not imported by production code.
## Testing Decisions
- Verify public behavior at the existing AppShell and management-page boundaries with MSW API fixtures; avoid tests of private state or CSS implementation details.
- Cover all five direct routes, browser history, host URL preservation, permission denial, guarded navigation and retained session behavior.
- Exercise workspace-specific preprocessing targeting and database-to-workspace navigation with authoritative API status fixtures.
- Use the existing authenticated Playwright stack to verify full pages, forms, container resize, narrow screens and a simulated portal header/sidebar. Fixtures avoid mutating the installed PSD knowledge.
- Run frontend typecheck/build, focused tests while implementing, and the full frontend test suite at integration.
## Out of Scope
Schema migrations, a new preprocessing service, Evidence web editing, new model configuration authority, actual deployment into Omics Portal, production deployment, and deletion of prototype variants. The only additional backend contract change in this UI increment is the pinned workspace identity in successful Resume responses.
## Further Notes
This implements the page direction recorded by ADR 0020, refined by the owner's
2026-09-12 acceptance of shelf A and explicit functional-preservation requirements.
Testing reuses the existing public UI seams. Ticket breakdown is recorded alongside
this spec as four independently reviewable increments. Gitea publication is pending
authenticated access; no remote issue identifiers are claimed by these local files.
@@ -0,0 +1,13 @@
# A1: Navigable Administration Pages
**What to build:** Workspace and Pi become pages; all five surfaces support deep links, browser history, permission enforcement and guarded navigation while retaining session state.
**Blocked by:** None (can start immediately).
**Status:** implemented and locally verified on 2026-09-12; local ticket, Gitea publication pending.
- [x] Five full-page surfaces reached from the current right navigation.
- [x] Refresh, Back/Forward and unrelated host URL/history fields preserved.
- [x] Unsaved edits block navigation; operations lock context without blocking page inspection. Unauthorized links denied.
- [x] Existing session continuation and management regressions pass.
- [x] One global shelf A; independent remembered workspace/model, validated defaults, session-pinned workspace on Resume.
@@ -0,0 +1,12 @@
# A2: Workspace preparation and Database dependency
**What to build:** An operator selects a workspace, inspects readiness and catalog revisions, runs preprocessing and follows a reciprocal link to Database configuration.
**Blocked by:** A1: Navigable Administration Pages.
**Status:** implemented and regression-tested locally on 2026-09-12; configured server acceptance remains in A4. Local ticket, Gitea publication pending.
- [x] Preprocessing runs against the globally selected workspace.
- [x] Missing, stale, blocked, running and failed status reflect the API.
- [x] Database shows the related workspace's readiness and links to its preparation.
- [x] Permission and confirmation regressions pass; no preprocessing/data-persistence behavior changed in this increment.
@@ -0,0 +1,12 @@
# A3: Shared A / Workbench family
**What to build:** Apply the chosen typography, page hierarchy and list/detail treatment to all five pages, preserving real domain operations and prototype alternatives.
**Blocked by:** A1: Navigable Administration Pages.
**Status:** implemented and locally verified on 2026-09-12; local ticket, Gitea publication pending.
- [x] Common heading, warm surfaces, controls and status hierarchy across five domains.
- [x] Memory/Evidence browsing and editing/maintenance behavior preserved.
- [x] Database configuration remains in the page work area; Pi status and host instructions are readable.
- [x] A/B/C prototype files and launch scripts preserved.
@@ -0,0 +1,15 @@
# A4: Embedded and responsive acceptance
**What to build:** Verify the complete family beside a portal sidebar and below its red header, including browser and keyboard navigation at narrow widths.
**Blocked by:** A2: Workspace preparation and Database dependency; A3: Shared A / Workbench family.
**Status:** local regression and browser acceptance completed on 2026-09-12; actual server/Omics integration gate remains open. Local ticket, Gitea publication pending.
- [x] Sampled actual application at 390, 768 and 1280 CSS px; responsive context and navigation visually checked with synthetic data.
- [x] Navigation reachable at mobile and desktop widths; original session scope tabs retained.
- [x] Typecheck, build, frontend/backend regression suites and targeted browser checks recorded.
- [ ] Standards/spec review completed; limits of actual portal integration documented.
- [ ] On-server integration under the real portal header/sidebar, including keyboard traversal, zoom and configured runtime operations.
Evidence and remaining gates: [implementation report](../../reports/2026-09-12-context-shelf-a-implementation.md).
@@ -0,0 +1,96 @@
# Administration follow-up: Gitea #28–#31
Baseline approved by the user: `f52bf22e` on `codex/prototype-administration-pages`.
Existing Admin, Core, sessions, and unified-model work is outside this review diff.
## Ticket acceptance
- [#28](https://git.tylconsulting.it/mptyl/ThothII/issues/28): Workspace page has four
tabs, in order: Preparation, Specific actions, About, Authentication. Every area's
content remains vertically accessible, with scrolling where required.
- [#29](https://git.tylconsulting.it/mptyl/ThothII/issues/29): Database opens at the
existing database list instead of automatically editing a connection. Owner comment:
remove the “Workspace preparation” footer and restore the table's original available
space and margins. Tables and columns remain accessible through the original grid actions.
- [#30](https://git.tylconsulting.it/mptyl/ThothII/issues/30): Pi fills the available
height minus its margins, scrolls overflowing content, and initially opens the tab
for the installation OS (Linux on a Linux server, macOS or Windows otherwise).
- [#31](https://git.tylconsulting.it/mptyl/ThothII/issues/31): Keep “management” in page
titles, remove it from Admin buttons; use “Pi configuration” for Pi.
## Scope and compatibility
The global workspace/model gate, operation locks, original Core, session navigation,
and unsaved-change guards are retained. The list currently retains the existing global
workspace scope; an asynchronous question asks whether the user wants to broaden this
to all workspaces. No cross-workspace operations are introduced without that decision.
Workspace tab panels stay mounted, preserving secret drafts and diagnostics. Pi platform
selection stays mounted across the two Pi sections and does not replace a manual choice
when status refreshes. OS information is projected by the host CLI, not inferred from
the browser. Older unprojected installations fall back to the backend runtime OS; see
the [operator guide](../general/pi-configuration.md#host-instructions-in-administration).
## Verification
TDD seams: AppShell navigation and public management-page behavior with MSW API fixtures;
the existing public Pi service and host projection contracts cover OS transport.
Red tests were observed before adding tabs, restoring list entry, selecting OS tabs,
projecting the host platform, and shortening navigation labels.
- Frontend full Vitest run: 82 files, 677 tests passed.
- Backend full Vitest run: 108 files passed, one skipped; 1,380 tests passed, 40 existing
optional tests skipped. Backend typecheck passed.
- After review fixes: all five affected frontend suites passed (163 tests, including
two new production-mode persisted-navigation regressions); frontend typecheck and
production build passed. The existing large-chunk advisory remains.
- The complete host CLI suite (`go test ./...`) passed across all 21 packages, including
the OS model-projection assertion.
- Strict documentation build passed.
- Read-only browser QA of actual React pages at 1280, 768 and 390 CSS pixels checked
Workspace tabs, Database table space, and Pi height/overflow. Pi's long instructions
initially exposed horizontal page overflow at 390 pixels; constrained grid children
and narrow definition lists corrected it. Database QA used an empty synthetic catalog;
the unit suites exercise populated tables, columns, edits and operation guards.
- Docker and real-stack Playwright acceptance were not run during initial implementation.
The preview is synthetic and read-only, not a deployment or provider smoke test.
## Standards
Initial independent review: one documented ADR conflict and one maintainability
judgement call. ADR 0020 now records the owner's superseding #29 footer decision.
Workspace and Pi now share keyboard tab navigation rather than duplicate its switch.
The independent follow-up review confirmed both findings resolved, with no new regressions.
## Spec
Initial independent review: two P2 findings. Fresh Database entry could still restore
Tables/Relationships from local storage, and a real-stack test retained the old Admin
button-label regex. Both were corrected; fresh entry is list-first while in-memory
navigation is retained when moving between already mounted app surfaces.
The independent follow-up review confirmed both findings resolved, with no new regressions.
Review summary: Standards 2 findings resolved, 0 outstanding; Spec 2 findings resolved,
0 outstanding. Both reviews were read-only, relative to the user-approved baseline.
## Local Docker deployment
At the owner's request, core and frontend were rebuilt from this checkout and recreated
on 2026-09-12 at 18:31 UTC using `/private/tmp/thothii-memory-preview.sh`, with
`--no-deps --force-recreate --wait`. Both are healthy. Database, Qdrant and embedding
containers retained their original IDs; no persistent volumes were deleted or recreated.
The normal model projection generator was run on this Mac before recreation; the running
core now receives `THT_HOST_PLATFORM=darwin`. The authored installation configuration and
credentials were unchanged. Before the update, images were retained as
`thothii-core:before-admin-28-31-20260912` and
`thothii-frontend:before-admin-28-31-20260912`; the prior projection directory was copied to
`/private/tmp/thoth-admin-28-31-docker.5ebAo8/generated`.
HTTP checks for the real UI (`http://127.0.0.1:8080/`), its `/api/health` proxy, and
the core `/health` returned 200. Served bundle `/index-Bsz7otBx.js` contains Workspace
areas and Pi configuration, and no longer contains the preparation footer or persisted
Database child-screen key. Authenticated end-to-end operator workflows were not rerun.
The Vite preview remains separate from Docker. Remote server deployment was not performed.
No Gitea issue state or remote branch was changed.
@@ -0,0 +1,96 @@
# Context shelf A: implementation and local acceptance
Date: 2026-09-12. Status: implemented and deployed to local Docker; not deployed to the server.
## Accepted scope
The owner selected the latest option A and explicitly required functional
preservation of Core and session management. Aesthetic refinement is limited to
the agreed Administration workbench and global context.
- Workspace, Evidence, Memory, Database and Pi open as peer pages in the work area.
- A single top collapsible shelf selects workspace and interaction model.
Explicit choices are independently remembered per origin, mount and principal;
installation defaults fill only missing choices. Invalid saved choices fail
closed instead of silently choosing another context.
- Core's composer, eight phases, review widgets and left activity log remain the
original production components. Session scope tabs, grouping, rename, archive,
bulk operations, read-only inspection and guarded Resume remain intact.
- Visited Admin pages and Core remain mounted during page navigation.
Unsaved Admin edits block navigation/context changes until saved or explicitly
cancelled in the editor. Failed saves and failed catalog refreshes preserve drafts.
- Workspace/model selectors are locked during operations. A navigation drawer
preserves access to the session rail when the log is open or space is narrow.
- The unified Installation Model Catalog remains the only model authority.
Core and metadata generation consume the same canonical selection.
Resume now includes the session's pinned `workspaceId` in successful responses,
binding the global workspace immediately without replacing the selected model;
older backends have a manifest fallback.
- Impeccable informed typography, warm surfaces, restrained red, spacing,
natural help-text wrapping and container-responsive layout. No Omics header
or left sidebar was added to production ThothII.
- A, B and C prototype sources and launch scripts were preserved.
## Verification
Using Node 24.16.0:
| Check | Result |
| --- | --- |
| Full frontend Vitest suite (two workers) | 671 tests passed, 82 files |
| Full backend Vitest suite (two workers) | Passed; existing 40 opt-in auth runtime projection tests skipped |
| Frontend TypeScript build check | Passed |
| Backend TypeScript check | Passed |
| Frontend production Vite build | Passed; bundle-size advisory remains |
| Git whitespace check | Passed |
| Strict MkDocs build | Passed |
Regression fixtures now provide valid context where they exercise normal Core
or session workflows. Separate tests explicitly cover invalid saved context,
global selector uniqueness, lock/unlock, failed and successful saves, retained
drafts across refresh failure, Core/Admin round trips, and pinned-workspace Resume.
The existing 40 session-management tests remain green.
Browser acceptance used the actual production React application against a
temporary read-only synthetic API, not the earlier prototype implementation.
Checked Core, expanded/collapsed shelf, Memory, session scope tabs and mobile
navigation at 390, 768 and 1280 CSS-pixel application widths. The test frame
scaled wider layouts to fit the inspection panel; layout width inside the frame
remained the stated CSS width. No real data, provider calls or runtime mutations
were performed. This is sampled visual acceptance, not certification of every
Admin editor on the real portal.
## Remaining release gates
### Local Docker update
At the owner's subsequent request, core and frontend were rebuilt from
`/Users/mp/projects/ThothII` and recreated in the existing
`thothii-18998cca7b0a` project. Its launcher preserves all previous Compose
bindings and adds `/private/tmp/thothii-context-a.compose.yaml` last to override
the old worktree build context. Only core/frontend were recreated; PostgreSQL,
Qdrant, embedding and persistent volumes were retained.
Verified HTTP 200 for the frontend and both direct/proxied core health endpoints.
The served production bundle contains the global shelf and unsaved-change guard,
without the synthetic preview API. All five persistent services report healthy.
The mounted catalog is schema v2, default `zai/glm-5.3`, with shared
DeepSeek Pro/Flash, local Qwen and GLM model identities.
Previous image tags are retained as `before-context-a-20260912` for each image.
This startup check does not exercise authenticated Core/provider or Admin workflows.
### Server release
1. Integrate on the server beneath Omics Portal's actual red header and beside
its existing sidebar. Supply available height through `--thoth-app-height`
when the host is shorter than the viewport. Check real host styles, widths,
zoom, keyboard traversal and Back/Forward.
2. Use approved test data to verify configured Core/Pi and Admin/LiteLLM models
through their real workflows. Unit fixtures do not certify provider compatibility.
3. Verify workspace readiness, synchronization and resume on the configured
stack before release. Follow the existing guarded server migration runbook.
4. Gitea publication and final independent standards/spec review remain separate
gates; no remote issues or deployment are claimed by this implementation.
The working tree also contains earlier model-catalog and Administration work.
Unrelated changes were preserved; this report does not claim a clean or committed tree.
@@ -0,0 +1,191 @@
# ThothII: survey e piano di revisione grafica
Data: 12 settembre 2026. Stato: **proposta, da approvare; nessuna modifica all'interfaccia**.
## Sintesi e direzione consigliata
L'incoerenza segnalata è confermata. Non dipende soltanto da dettagli sfuggiti nelle singole pagine: il design system attuale prescrive tre registri tipografici, ai quali alcuni componenti aggiungono un secondo stack serif. Titoli, etichette operative, documenti e istruzioni finiscono così per sembrare parti di prodotti diversi.
Propongo una direzione unica: **interfaccia operativa interamente sans-serif, rosso istituzionale come accento, colori aggiuntivi soltanto quando comunicano uno stato, maggiore coerenza senza impoverire le informazioni**. Non serve una nuova impaginazione del Core né una nuova collocazione delle sessioni.
Ho usato **Impeccable**, nel registro product, per orientare la valutazione di gerarchia, tipografia, densità, colore e microcopy. La varietà visiva dovrà venire da spaziatura, pesi e organizzazione, non dall'alternanza di famiglie tipografiche. Le grazie non sono intrinsecamente incompatibili con un prodotto tecnologico, ma qui contrastano con la direzione richiesta e non svolgono una funzione necessaria: le eliminerei dalla UI, compresi i lettori di documenti.
## 1. Metodo, copertura e limiti
Verifica tramite Playwright nel browser dell'app, con screenshot, navigazione e lettura degli stili CSS calcolati. La survey principale riguarda la **versione Docker reale su `http://127.0.0.1:8080/`**, autenticata con l'account autorizzato. Una precedente esplorazione dell'anteprima sintetica non è usata come prova dei dati o del funzionamento del Docker.
| Area | Verifica sulla versione reale |
| --- | --- |
| Workspace | Preparation, Specific actions, About, Authentication |
| Database | Elenco database, navigazione alle tabelle e alle colonne |
| Memory | Elenco vuoto e apertura del form New card, chiuso senza modifiche |
| Evidence | Fonti/importazione, elenco, dettaglio di un documento |
| Pi | Runtime e Host maintenance, scheda macOS |
| Core | Stato iniziale, compositore e controlli presenti senza avviare una domanda |
| Sessioni | My sessions, All sessions, archivio e lettura di una sessione archiviata |
| Navigazione | Drawer responsive, accessi Admin e contesto globale |
Non ho salvato form, lanciato elaborazioni, ripreso sessioni, importato documenti o modificato configurazioni. Il controllo del Core attivo, delle fasi, dei gate e del log è limitato alla lettura del codice: non è stato eseguito un ciclo LLM per una survey grafica.
Il viewport misurato nel pannello browser era **649 × 1091 CSS px**. Questo offre evidenza concreta del comportamento a larghezza ridotta, ma non sostituisce una matrice desktop/mobile. Tema scuro, contrasto numerico, navigazione completa da tastiera, zoom e integrazione effettiva in Omics Portal restano da verificare. Non è una certificazione WCAG né un audit automatizzato completo. Le famiglie riportate sono gli stack CSS calcolati, non una verifica del font effettivamente caricato dal sistema.
## 2. Problemi riscontrati
### Tipografia: quattro stack e gerarchie variabili
| Esempio osservato | Trattamento attuale | Problema |
| --- | --- | --- |
| Titoli delle pagine Admin | Fraunces/serif, 30 px | Registro editoriale molto più marcato del resto dell'app |
| Sources and imports; titolo del form Memory | Altro stack `ui-serif`, 18–20 px | Nemmeno i titoli serif usano tutti la stessa famiglia |
| Evidence library; Original question; Revised question | Monospace, 11 px | Etichette ordinarie trattate come identificatori tecnici |
| Working context | Monospace maiuscolo, 10 px | Informazione globale importante resa molto piccola |
| Guida Pi Host maintenance | Testo 12 px, interlinea 16 px; titoletti serif 12 px | Istruzioni lunghe troppo dense e gerarchia debole |
| Sessione archiviata | Titolo sessione serif 14 px; titolo nel documento serif 23,2 px | Il contenuto sovrasta il contesto del pannello |
La causa è documentata in `DESIGN.md`, nella regola dei tre registri. Nel codice, `frontend/src/index.css` assegna globalmente il font heading a `h1`–`h6` e ai titoli della prosa; Memory ed Evidence aggiungono classi `font-serif`. Il problema ricompare quindi anche quando un componente non richiede esplicitamente un titolo decorativo.
### Colore: troppi significati impliciti
- In Database il tipo di connessione REST e la data di aggiornamento ricevono accenti blu pur essendo informazioni ordinarie.
- La guida ordinaria di manutenzione Pi è racchiusa in una cornice ambra, visivamente simile a un avviso. Il colore non distingue un pericolo concreto.
- Gli stati Ready in Pi usano anche testo verde molto chiaro: è un rischio di leggibilità da misurare, non una violazione numericamente accertata in questa survey.
- Stato operativo, azione primaria, contenitore informativo e semplice categoria non seguono una grammatica cromatica sufficientemente riconoscibile.
Non eliminerei invece indiscriminatamente verde e ambra: successo, errore, attesa di revisione e avvertimento devono restare distinguibili, sempre anche tramite testo o icona.
### Spazio e linguaggio
| Area | Evidenza | Intervento proposto |
| --- | --- | --- |
| Workspace | Identità e descrizioni ripetute fra contesto, selezione e intestazioni; molte gerarchie prima delle azioni | Mantenere le quattro schede, ridurre ripetizioni e uniformare sezioni e azioni |
| Database | Intestazione, stato, cronologia, Fleet summary, breadcrumb e toolbar precedono i dati; a 649 px il riepilogo occupa più righe | Compattare il riepilogo, dare priorità alla griglia; sostituire il gergo Fleet con un nome corrispondente al suo ambito |
| Memory | Stato vuoto formulato come assenza di risultati filtrati anche senza carte; ampie aree vuote e paginazione disabilitata | Distinguere libreria vuota da ricerca senza risultati; stesso linguaggio visivo di toolbar e form del resto dell'Admin |
| Evidence | Percorso tecnico e istruzioni di importazione dominano la parte superiore; ricerca compressa rispetto agli altri filtri | Istruzioni brevi con dettagli espandibili; percorso secondario ma copiabile; filtri che si dispongono su più righe prima di diventare illeggibili |
| Pi | Guida molto lunga e piccola; spiegazioni dell'architettura mescolate con istruzioni all'operatore | Separare stato, azioni e guida; rendere la guida leggibile e consultabile per attività |
| Sessioni | Molti registri tipografici; a 649 px il pannello documenti è stretto e tronca il titolo | Coerenza dei titoli e dei documenti; adeguare il pannello alla larghezza disponibile senza cambiare la gestione delle sessioni |
| Core e navigazione | Branding ripetuto, microetichette e controlli di taglie diverse | Riallineare la gerarchia e i componenti condivisi, conservando struttura e comportamento |
Gli stili comunicativi oscillano fra introduzione promozionale nel Core, manuale tecnico in Pi/Evidence e terminologia da dashboard infrastrutturale in Database. La soluzione è un tono operativo unico, non la rimozione delle informazioni tecniche necessarie.
## 3. Regole del sistema visivo proposto
### Una famiglia UI, cinque ruoli dimensionali
Consiglio di **mantenere Manrope**, già adottato per l'interazione, e usarlo per tutta la UI. Non aggiungerei una nuova famiglia per il solo gusto di cambiare. Pesi ordinari 400, 500 e 600; monospace soltanto per SQL, codice, percorsi e identificatori quando serve riconoscerli o copiarli.
| Ruolo | Dimensione proposta | Uso |
| --- | --- | --- |
| Titolo pagina | 24 px / 1,5 rem, peso 600 | Core/Admin, senza effetto copertina |
| Titolo sezione | 20 px / 1,25 rem, peso 600 | Sezioni principali e pannelli di lettura |
| Lettura | 16 px / 1 rem, peso 400 | Risposte, documenti, istruzioni articolate; interlinea circa 1,5 |
| Interazione e dati | 14 px / 0,875 rem, peso 400–600 | Form, tab, pulsanti, griglie; sottosezioni compatte in peso 600 |
| Metadati secondari | 12 px / 0,75 rem, peso 400–500 | Timestamp, conteggi e indicazioni accessorie |
Sono ruoli, non l'obbligo di rendere ogni titolo di sezione grande: una sottosezione compatta può usare il ruolo da 14 px in semibold. Niente ridimensionamento fluido che faccia oscillare continuamente i caratteri con la larghezza. Prima si riorganizza il layout, poi si valutano eccezioni documentate.
Ulteriori regole:
- Eliminare Fraunces e le classi serif dall'interfaccia e dalla resa dei documenti. Non riscrivere il contenuto dei documenti.
- Usare etichette sans-serif in sentence case; limitare maiuscole e spaziatura espansa.
- Nessun testo operativo ordinario sotto 12 px; non far stare più contenuto riducendolo a 8–11 px.
- Numeri tabulari nelle colonne numeriche; testo libero proporzionale; codice monospace leggibile.
- Distribuire il font con l'app, evitando che Google Fonts sia necessario alla coerenza della UI in un ambiente intranet. Verificare fallback e caricamento, senza presumere che oggi il font remoto non funzioni.
### Palette: neutri e rosso, con eccezioni semantiche
| Ruolo | Regola |
| --- | --- |
| Superfici | Bianco e pochi grigi neutri, bordi leggeri; niente colorazione per ogni sezione |
| Testo | Grafite per testo principale, grigio leggibile per secondario |
| Rosso istituzionale | Azione primaria, selezione e focus; tonalità coerente con il tema Omics, da verificare nell'integrazione |
| Successo | Indicatore verde sobrio con testo sufficientemente scuro; non interi paragrafi verde pallido |
| Avvertimento/attesa umana | Ambra soltanto quando occorre attenzione, con motivazione esplicita |
| Errore/distruzione | Trattamento distinto tramite icona, etichetta e conferma dove prevista; non affidarsi al solo rosso condiviso con il brand |
| Informazioni ordinarie | Neutre: timestamp, protocollo, conteggi, guide e configurazione non sono automaticamente stati colorati |
SQL, grafici e visualizzazioni possono mantenere colori utili alla comprensione dei dati. Questa revisione non impone un monocromo indiscriminato. Gli stati del workflow conserveranno il loro significato; ogni modifica cromatica dovrà avere una mappatura esplicita.
### Componenti e testi condivisi
- Un'unica famiglia di intestazioni Admin, toolbar, tab, campi, pulsanti, badge e messaggi.
- Scala di spaziatura comune basata su multipli di 4 px, con distinzione chiara fra spazio dentro un gruppo e spazio fra gruppi.
- Una primaria per il compito locale; azioni secondarie neutre, pericolose riconoscibili senza rendere tutto rosso.
- Tab allo stesso livello con lo stesso trattamento. Le schede OS di una guida possono essere più compatte, ma non appartenere a un altro sistema grafico.
- Istruzione essenziale accanto all'azione; procedure e dettagli di implementazione in una sezione di aiuto espandibile.
- Testi UI in inglese; documenti e dati conservano la propria lingua. I titoli italiani di Evidence e sessioni non sono un errore di uniformità.
Esempi di microcopy da affinare nell'implementazione:
| Attuale | Proposta |
| --- | --- |
| Fleet summary | Catalog summary, se confermato l'ambito del riepilogo |
| No cards match these filters… in una libreria vuota | No memory cards yet. Create a card to add reusable knowledge. |
| Session models in Pi | Available models, con indicazione dell'effettiva idoneità Core/Admin |
| Spiegazione che la pagina “never creates a second model default” | Installation default, con breve indicazione di dove modificarlo e dettagli tecnici separati |
Il pulsante che prova il default d'installazione deve continuare a dichiarare **quale modello testa**: non va confuso con il modello selezionato nel contesto globale. Anche la distinzione fra importazione, revisione e consolidamento Evidence deve rimanere esplicita.
## 4. Vincoli funzionali e integrazione
La revisione deve conservare:
1. Core originale: domanda/risposta, otto fasi in alto, gate di revisione, log sul lato sinistro e controlli esistenti.
2. Sessioni nella navigazione destra con **My sessions / All sessions**, archivio e azioni attuali. Nessuna sezione sostitutiva in fondo alla pagina.
3. Contesto globale A espandibile in alto: workspace e modello unici per tutta l'app; scelte ricordate indipendentemente e default esistenti.
4. Blocco delle attività senza contesto valido e del cambio contesto durante un'elaborazione; protezione delle modifiche Admin non salvate.
5. Cinque pagine Admin, preprocessing in Workspace Preparation e gestione database/tabelle/campi/relazioni in Database.
6. Unificazione già realizzata della configurazione LLM, senza introdurre selettori o default alternativi.
7. Prototipi precedenti conservati e recuperabili.
**Omics Portal possiede header rosso e sidebar sinistra.** ThothII non deve duplicarli. Le regole globali attuali su `:root`, `body` e titoli richiedono una verifica di isolamento: se l'integrazione avviene nello stesso DOM, gli stili vanno circoscritti alla radice ThothII; se avviene in iframe, vanno verificati dimensionamento, tema e scrolling. È un rischio di integrazione da verificare, non una sovrapposizione già osservata sul portale.
A larghezze ridotte pannelli e filtri devono adattarsi alla larghezza del contenitore reale, non soltanto della finestra. Nel lettore della sessione il testo non dovrebbe restare confinato in una colonna di circa 195 px come nello screenshot a 649 px: prevedere un pannello più ampio o sovrapposto nella modalità compatta, mantenendo accessi e funzione originali.
## 5. Sequenza di lavoro proposta
### Fase 1: regole e campione rappresentativo
Dopo approvazione, aggiornare `DESIGN.md` eliminando la regola dei tre registri e allineando gli eventuali riferimenti superati ai modelli e alla navigazione. Definire token tipografici, colori semantici e componenti comuni.
Applicare un primo campione a **Workspace e Pi**: insieme verificano form, stato operativo, selezioni, istruzioni lunghe e codice. Mostrare il confronto prima/dopo mantenendo stessi contenuti e viewport. Non servono tre nuovi design alternativi: serve validare un sistema coerente.
### Fase 2: shell e tutte le pagine Admin
Uniformare contesto globale, navigazione, titoli, tab e controlli; estendere il sistema a Database, Memory ed Evidence. Compattare riepiloghi e istruzioni senza eliminare funzionalità. Correggere stati vuoti, larghezze dei filtri e formattazione dei blocchi di codice nella guida Pi.
### Fase 3: Core, documenti e sessioni
Applicare gli stessi token a compositore, fasi, log, widget di revisione, documenti e navigazione sessioni. Nessuna nuova struttura del workflow. Controllare separatamente liste, documento aperto, sessione attiva e pannelli nella modalità compatta.
### Fase 4: verifica e consegna
Eseguire typecheck, test dei componenti interessati e regressione Playwright con dati di test, senza consumare chiamate LLM reali soltanto per verificare gli stili. Includere prove di flusso per navigazione con modifiche non salvate e blocco del contesto durante un'operazione.
Preparare screenshot comparabili e verifiche a 390, 649, 768, 1280 e 1600 px, oltre alla larghezza effettiva assegnata da Omics con sidebar presente. Verificare tema chiaro/scuro, zoom 200%, tastiera, focus, testo lungo, stato vuoto/errore/caricamento e griglie larghe. L'integrazione finale nel server resta una fase distinta, secondo le decisioni già prese.
## 6. Criteri di accettazione
- Una sola famiglia sans per la UI; monospace solo per contenuto tecnico. Nessuna classe serif residua nei componenti operativi o nei lettori.
- Dimensioni e pesi derivati dai ruoli condivisi; ogni eccezione ha un motivo documentato.
- Colori dei componenti da token semantici; nessun blu decorativo per date o protocolli e nessuna cornice di warning intorno a una guida ordinaria.
- Contrasto del testo almeno 4,5:1, salvo le eccezioni previste per testo grande e altri casi specifici; non assumere che ogni semibold sia testo grande. Riferimento: [W3C, Contrast Minimum](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html).
- Contrasto almeno 3:1 per informazioni visive necessarie a identificare controlli e stati, nei casi applicabili; non è un obbligo per ogni bordo decorativo. Riferimento: [W3C, Non-text Contrast](https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html).
- Stato e azione distinguibili anche senza colore; focus visibile e controlli utilizzabili da tastiera.
- Nessuna sovrapposizione o scroll orizzontale dell'intera pagina ai viewport concordati; scroll locale consentito per tabelle e codice quando necessario.
- Nessuna regressione funzionale di Core, sessioni, Admin, selezione globale o salvataggio; test dei flussi critici invariati o rafforzati.
- Nessuna modifica del contenuto scientifico, dei default LLM, delle autorizzazioni o delle procedure di manutenzione mascherata da intervento grafico.
## 7. Punti principali del codice interessati in una futura implementazione
- `DESIGN.md`: regole tipografiche e cromatiche.
- `frontend/index.html`: caricamento delle famiglie di caratteri.
- `frontend/src/index.css`: token, titoli globali, etichette e prosa.
- `frontend/src/shell/WorkingContextShelf.css`: contesto globale e microtipografia.
- `frontend/src/shell/administration/AdministrationPage.css`: intestazioni comuni.
- `frontend/src/shell/WorkspaceManager.css`: gerarchie e densità Workspace.
- `frontend/src/shell/database-management/FleetLedgerShell.css`: riepiloghi, toolbar e griglie.
- Componenti Memory/Evidence: override serif di titoli e form.
- Componenti Pi: guida, stati, tab OS e microcopy operativo.
- Componenti Core/sessioni: fasi, lettura documenti, log, navigazione e pannelli responsive.
**Decisione richiesta:** approvare questa direzione visiva e il campione Workspace/Pi prima di estenderla. Questa survey non crea ticket, non aggiorna il design normativo e non ricostruisce Docker.
@@ -0,0 +1,175 @@
# Revisione grafica: consegna su worktree e Docker
Stato: **pubblicata per revisione visiva, non integrata in main**.
## Dove provarla
- Docker locale: <http://127.0.0.1:8080/>.
- Worktree: `/Users/mp/projects/ThothII-visual-review`.
- Ramo: `codex/ui-visual-review`.
- Base preservata: `2d1b714ebe31419d712e9c3324a5e171d5f0317d`.
- Checkout originale: `/Users/mp/projects/ThothII`, ramo `codex/prototype-administration-pages`, lasciato intatto, incluse le note non ancora committate e i prototipi.
Non è stato eseguito alcun merge, push o aggiornamento di Gitea. Questa consegna richiede la revisione ottica del proprietario prima dell'adozione.
## Interventi
Impeccable, registro product, ha guidato le scelte: Manrope sans-serif locale per tutta la UI; niente serif nei componenti o nei documenti; cinque ruoli tipografici condivisi; monospace conservato per contenuto tecnico. I font non dipendono più da Google Fonts.
Le etichette ordinarie non usano più monospace maiuscolo. Le piccole dimensioni arbitrarie dei componenti sono state ricondotte a 12, 14 o 16 px. Titoli Admin a 24 px; intestazioni, controlli e prosa sono coerenti fra superfici.
Il rosso resta l'accento principale. Date e protocolli sono neutri. Verde e ambra usano token semantici leggibili anche in tema scuro. La guida Pi non è più presentata come un grande avviso ambra. Il tema delle utility segue il tema dell'app/portale, non autonomamente quello del sistema operativo. Placeholder e focus sono più leggibili.
Interventi specifici:
- Workspace: tipografia e stati uniformati, mantenute quattro schede e preprocessing.
- Database: riepilogo più compatto, Catalog summary al posto di Fleet summary, intestazioni delle griglie senza monospace maiuscolo, selezioni compatibili con il tema scuro.
- Memory: titoli e form sans-serif; messaggio distinto per libreria vuota e nessun risultato filtrato; paginazione mostrata quando serve.
- Evidence: istruzioni tecniche espandibili, percorso host sempre disponibile e copiabile nella relativa sezione, filtri responsive con ricerca di larghezza utile.
- Pi: guida leggibile a 16 px, codice correttamente su più righe, tab OS coerenti, testo che distingue default d'installazione e modello selezionato, Available models.
- Core/sessioni: uniformazione estetica senza modifiche al workflow; otto fasi, gate, log, input e gestione sessioni conservati. Nei layout compatti il lettore documenti usa lo spazio disponibile anziché una colonna illeggibile.
Il modello globale, i default, le autorizzazioni e i blocchi di navigazione non sono stati ridisegnati. Non sono state eseguite elaborazioni LLM o mutate informazioni applicative per questa verifica.
## Follow-up: marchio applicativo (2026-09-13)
Su richiesta dell'utente, ripristinato ThothII a 48 px nel Core e ingrandito a 32 px nella
sidebar destra. Manrope semibold e suffisso II rosso restano coerenti con Impeccable;
il marchio è un'eccezione esplicita alla scala dei titoli ordinari in DESIGN.md.
Nessuna modifica funzionale. Build e 7 scenari Playwright superati, con controlli delle
dimensioni del marchio alle cinque larghezze. Pubblicato nuovamente il solo frontend
Docker usando il medesimo tag di revisione; immagine di ripristino invariata, nessun merge.
La nuova immagine sostituisce quella identificata nella prima consegna sotto:
`sha256:f576ee4882456b4d2d338dbb2228d301f9ffd8d0c7de541842c767bbe8d02dad`.
## Follow-up: indicatori del catalogo (2026-09-13)
Spostati i pallini dopo le etichette Catalog, Operations e Metadata updated, mantenendo
il rientro del testo e l'allineamento con i valori sottostanti. Spaziatura uniforme e
centratura verticale secondo Impeccable. I 7 scenari Playwright passano, inclusi nuovi
controlli geometrici per tutte e tre le etichette alle cinque larghezze responsive.
Il successivo aggiornamento del medesimo tag frontend include questa correzione;
il digest del follow-up precedente identifica quindi una versione superata.
## Follow-up: altezza del prompt Core (2026-09-13)
Riprodotto con Playwright il campo schiacciato: altezza inline 0 px, spazio utile 8 px,
a fronte di 31 px necessari per la prima riga. L'autosizing misurava il Core ancora
nascosto durante il caricamento del contesto e non reagiva alla successiva apertura.
Il reset isolato dell'altezza ad auto riportava immediatamente il campo a 31 px.
Il composer ora evita misure nascoste, ricalcola al cambio di larghezza/visibilità e
al caricamento dei font, conserva un minimo di 32 px e cresce fino a 160 px con scroll.
Il focus usa il contorno esterno esistente, senza il secondo bordo interno.
Nessuna modifica all'invio o al workflow. Passati 9 scenari Playwright e 17 test Vitest
mirati; coperti ingresso dal Core e dall'Admin, Shift+Enter, testo lungo, mobile,
navigazione con bozza conservata e svuotamento. Build Docker frontend aggiornata;
branch di revisione isolato, immagine di ripristino invariata.
## Follow-up: griglia di allineamento del catalogo (2026-09-13)
Catalog condivide il margine di Catalog summary; sui layout ampi Metadata updated
condivide l'asse di Description coverage e Operations occupa il punto medio fra i due.
Usati margini condivisi e gli stessi cinque riferimenti di colonna dei KPI, senza offset
assoluti. Sulle larghezze ridotte gli stati continuano a disporsi senza sovrapposizioni.
Passati 9 scenari Playwright, con assert geometrici sugli allineamenti richiesti.
Pubblicazione limitata al frontend Docker del worktree di revisione, nessun merge.
## Follow-up: margini esterni e assi interni (2026-09-13)
La verifica geometrica iniziale rilevava 20 px di differenza fra il bordo sinistro
del riepilogo e quello della tabella. In accordo con Impeccable, introdotti due token:
margine esterno condiviso di 20 px e padding interno di 16 px. Catalog summary,
navigazione Databases e tabella hanno bordi sinistro/destro coincidenti; riepilogo,
Tables, Databases e Workspace databases condividono l'asse testuale a 36 px.
Header, toolbar azioni e stati seguono lo stesso asse. Conservato l'allineamento
Metadata updated/Description coverage e Operations al punto medio sui layout ampi.
Passati 9 scenari Playwright con assert geometrici a 390, 649, 768, 1280 e 1600 px.
Nessuna modifica funzionale; aggiornamento Docker limitato al frontend di revisione.
## Follow-up: pannello del contesto globale (2026-09-13)
Il pannello espanso Workspace/Model ora usa gli stessi token di margine esterno (20 px)
e padding interno (16 px) del catalogo, con superficie rientrata e controlli sull'asse
testuale a 36 px. Allineamento curato con Impeccable e verificato alle cinque larghezze.
Rimosse la frase sulle ultime scelte e le indicazioni di provenienza sotto i selettori
(Installation default / Last choice). Logica di selezione, persistenza, default,
blocchi operativi e messaggi di errore invariati. Passati 9 scenari Playwright con
controlli sul pannello aperto, sui margini e sull'assenza delle diciture rimosse.
## Verifica
| Verifica | Esito |
| --- | --- |
| Frontend, suite Vitest | 679 test passati |
| TypeScript + build produzione | Passati |
| Playwright dedicato | 7 scenari passati, nessun retry |
| Larghezze | 390, 649, 768, 1280, 1600 CSS px |
| Superfici Playwright | Cinque Admin, guida Pi, Core iniziale, My/All sessions e lettore archivio |
| Interazioni | Tab OS da tastiera, modifica Memory trattenuta durante tentativo di navigazione, ritorno al Core dopo annullamento |
| Tema | Chiaro indipendente dal tema OS; scuro tramite attributo del portale |
| Spazio host | Simulazione di 240 px riservati al menu sinistro e 72 px all'header, con utility concorrente |
| Docker reale | Accesso e navigazione fra le cinque pagine Admin; titolo sans-serif 24 px e nessun overflow dell'intera pagina nel viewport verificato |
I test visuali usano fixture e intercettano tutte le richieste API prima che possano raggiungere un'installazione. Nessuna scrittura è ammessa: ogni scenario controlla che non ne siano state tentate. Gli screenshot sono negli output locali ignorati da Git, `frontend/test-results/visual-review/`.
Comandi di verifica nel worktree:
```sh
cd /Users/mp/projects/ThothII-visual-review/frontend
npm test -- --maxWorkers=2 --minWorkers=1
npm run build
npx playwright test --config playwright.visual.config.ts
```
Contrasti calcolati sulle coppie di token opachi di riferimento, non su ogni possibile composizione/hover:
| Coppia | Chiaro | Scuro |
| --- | --- | --- |
| Testo principale / card | 15,16:1 | 13,61:1 |
| Testo secondario / card | 5,67:1 | 8,40:1 |
| Successo / card | 6,76:1 | 8,69:1 |
| Avvertimento / card | 6,64:1 | 9,44:1 |
| Bordo input / card | 3,36:1 | 4,56:1 |
| Testo pulsante primario / riempimento | 5,03:1 | 6,83:1 |
Limiti: non è una certificazione WCAG completa; non è stata eseguita una nuova sessione LLM sul Docker reale. La prova con geometria host non equivale all'integrazione nel vero Omics Portal. Lo zoom browser 200% e tutte le combinazioni di hover/trasparenza non sono stati verificati esaustivamente. I test funzionali già esistenti coprono fasi, sessioni e protezioni; le modifiche non cambiano la loro logica.
## Docker e ripristino
Progetto Compose: `thothii-18998cca7b0a`. È stato ricreato **solo frontend** con `--no-deps --no-build` dopo una build esplicita dal nuovo worktree.
| Elemento | Valore verificato |
| --- | --- |
| Immagine nuova | `thothii-frontend:visual-review-20260912` |
| Digest immagine nuova | `sha256:eeccb28f5c5cd78da08fd007e259e3fb88f26b4f1415a825a3b5829061f79ed3` |
| Immagine precedente conservata | `thothii-frontend:before-visual-review-20260912` |
| Digest precedente | `sha256:635f25477f11a476034052e87b2129ae26a9a049c4bedde0ada3a1d65e4ee924` |
| Container frontend nuovo | `50dd012cd400`, healthy |
| Core invariato | `0dcce917be0c`, healthy |
| Catalog DB invariato | `89755e439dc7`, healthy |
| Qdrant invariato | `52ebd5c47955`, healthy |
| Embedding invariato | `7f690e534eda`, healthy |
| Bundle servito | `/index-Cd-Ma0ye.js`, `/index-CptC2K6s.css` |
Per tornare al look precedente, senza rebuild e senza modificare dati o Core:
```sh
bash /private/tmp/thothii-memory-preview.sh \
-f /Users/mp/projects/ThothII-visual-review/deploy/compose.visual-review-rollback.yaml \
up -d --no-deps --no-build frontend
```
Per ripubblicare il look di revisione già costruito:
```sh
bash /private/tmp/thothii-memory-preview.sh \
-f /Users/mp/projects/ThothII-visual-review/deploy/compose.visual-review.yaml \
up -d --no-deps --no-build frontend
```
Questi override sono deliberatamente locali e usano il launcher dell'installazione già presente; non vanno applicati sul server PSD. Il launcher in `/private/tmp` dipende dai profili locali esistenti: se rimosso, ricostruire l'invocazione dai profili dell'installazione, senza sostituire configurazioni o volumi. Le immagini di review e rollback restano taggate: non eseguire una pulizia delle immagini prima della decisione.
## Decisione successiva
Il proprietario valuta il look su Docker. Solo dopo approvazione si procederà con l'integrazione su `main`, verificando anche l'eventuale differenza fra `main` e il ramo di partenza. Un rifiuto della revisione richiede soltanto il ripristino frontend sopra indicato; codice e prototipi precedenti sono preservati.
@@ -0,0 +1,80 @@
# Unified interaction LLM — local implementation
Date: 2026-09-12. This change is separate from selection of the final administration/context-shelf
prototype. No deployment, Gitea issue publication, or Omics Portal modification was performed.
## Implemented
- One authored `modelCatalog.defaults.interaction` per installation, independent of workspace.
The local PSD descriptor and tracked example now use `zai/glm-5.3` in this field.
- One generated `defaultInteraction` in runtime catalog schema v2; host projections, backend
settings, Pi diagnostics, Core admission and metadata-generation consumers use that value.
- Separate Pi/LiteLLM adapters remain. When Admin AI is configured, both public lists and Pi's
enabled list use their intersection. Single-use entries remain inventory, not global choices.
Core-only installations remain possible when no Admin model is configured. Embedding is unchanged.
- Equal legacy defaults normalize on read without rewriting the source; divergent defaults or
mixed old/new fields fail with migration guidance. Generated schema-v1 catalogs must be regenerated.
- The existing Core and Database selectors now share application preferences, using complete
canonical IDs, including when two providers use the same short model name.
- Explicit LLM choices are remembered in browser storage per origin/application mount and
authenticated issuer/subject. Automatic defaults are not stored as explicit choices. Workspace
and thinking preferences retain their existing in-memory lifetime.
- Core/description-generation activity disables the existing model controls using existing state
and the shared run cache. This is not a new cross-workspace/background job subsystem.
- New sessions reject unavailable selections without fallback. Resume receives the global model
(or uses the installation default when omitted), keeps the historical workspace/revision and does
not overwrite the original manifest model fields. Archived/finalized sessions remain read-only.
- Pi management instructions and the configuration guide require an operator to exercise each
model in both Core and Administration. There is no automatic certification flag or live probe.
## PSD DeepSeek consolidation
Following explicit operator approval, the local PSD descriptor and its tracked example use one
`deepseek` provider with native Pi and LiteLLM adapters. Both `deepseek/deepseek-v4-pro` and
`deepseek/deepseek-v4-flash` are eligible for shared selection. The duplicate `deepseek-metadata`
provider declaration is removed; historical records remain unchanged. The installation default
stays `zai/glm-5.3`.
A value-free comparison confirmed that the existing Pi DeepSeek key and the existing
`DEEPSEEK_API_KEY` bundle entry are identical; both source files have mode 0600. No secret file
was changed. The catalog now declares `secret_env` / `DEEPSEEK_API_KEY` as the authoritative
source for both paths. Pi session snapshots exclude the selected provider's old auth entry,
without changing the original store or unrelated entries. Missing bundle keys fail closed instead
of falling back to old Pi auth or the generic key file. Availability enumeration, credential status,
and isolated provider smoke checks follow the catalog declaration as well.
## Verification
- All host CLI Go packages pass `go test ./...`.
- Backend under Node 24.16.0 after DeepSeek consolidation: 108 test files pass, 1 skipped;
1,377 tests pass, 40 skipped.
Backend TypeScript check passes.
- New regression coverage checks shared DeepSeek configuration/projection, canonical Core/Admin
identities, bundle-backed model enumeration, stale Pi auth precedence, missing-key refusal,
preservation of the source auth store, and sanitized credential status. All credentials in these
tests are synthetic; no model inference is performed.
- Full frontend run: 658 tests pass; three remaining failures concern existing administration-layout
work: Workspace preprocessing region, Catalog status placement, and Sync history entry point.
These controls were already being changed in the dirty worktree before this model change.
- The focused Core/Admin model synchronization test also verifies locking during Core and
description-generation activity, unlocking afterward, and provider-qualified identity. It passes.
- Frontend typecheck still reports three `exact` option errors in the pre-existing, untracked
`AppShell.administration.test.tsx` (lines 48, 66, 71). No new type errors are reported.
- Earlier parallel runs encountered timing-sensitive auth-helper failures; the complete backend
rerun with Node 24 and two workers passes. The Homebrew `node@24` path on this machine actually
reported Node 25; the verified Node 24 executable is under the user's nvm installation.
- No live provider calls or full deployed Pi/LiteLLM/Omics acceptance tests were performed.
- `git diff --check` passes. Strict documentation build remains blocked by an existing link in
the untracked `plans/2026-09-10-administration-pages-spec.md` pointing outside the documentation
tree to the administration-review prototype README. The updated configuration guide adds no warning.
The full frontend gate is not green; this is not a deployment-ready acceptance claim. Pre-existing
production UI edits and every prototype are preserved. The final collapsible global context layout,
general Core/Admin admission policy and server assembly remain in the wider UI workstream.
## Applying later
Use the matching host/backend release, review the installation descriptor, then apply the normal
installation lifecycle to regenerate **all** runtime projections together. Do not deploy only the new
backend against an old generated catalog. Follow the dual-path operator checklist in
[Installation Model Catalog](../general/pi-configuration.md).
@@ -0,0 +1,122 @@
# Google Antigravity e Gemini 3.8 Flash: convenienza costo/qualità
Verifica effettuata il **7 settembre 2026**, privilegiando documentazione e annunci
ufficiali Google. Le prove indipendenti su Gemini 3.8 Flash sono ancora limitate perché il
modello è stato pubblicato il 2 settembre 2026.
## Giudizio sintetico
- **Registrazione gratuita: decisamente conveniente.** Il piano Individual da $0 include
Gemini 3.8 Flash, l'ultima versione Flash, oltre alla CLI e alle funzioni principali di
Antigravity. È difficile ottenere un rapporto costo/qualità migliore di un accesso gratuito
a un modello di questa fascia.
- **Google AI Pro da circa $20/mese: probabilmente conveniente per uso regolare**, ma solo dopo
aver misurato il consumo sul piano gratuito. Google non pubblica un numero fisso di prompt o
token inclusi, quindi non è possibile calcolare un break-even affidabile contro l'API.
- **Ultra da $100 o $200: non lo comprerei per il solo Gemini Flash** senza aver già dimostrato
di saturare Pro. Il piano da $100 offre 5 volte la quota Pro; quello da $200 ne offre 20 volte.
- Per il server dietro CyberArk, Antigravity ha un vantaggio concreto: la CLI ufficiale `agy`
funziona direttamente in Linux e prevede esplicitamente un flusso OAuth remoto tramite URL e
codice, senza tunnel né port forwarding.
## Accesso e prezzi
La [pagina ufficiale dei piani Antigravity](https://www.antigravity.google/docs/plans/) e la
[tabella dei modelli](https://www.antigravity.google/docs/models/) indicano che Gemini 3.8 Flash
è disponibile su Individual gratuito, AI Plus, AI Pro, AI Ultra ed Enterprise. Il piano gratuito
include anche completamenti Tab illimitati e tutte le funzioni del prodotto, inclusa la CLI, ma
ha un limite settimanale di base.
Google AI Pro costa normalmente **$19,99/mese** nella pagina internazionale di
[Google One](https://one.google.com/about/plans) e offre quote Antigravity superiori, con rinnovo
ogni cinque ore finché non viene raggiunto il limite settimanale. Google AI Ultra è offerto a
[$100/mese con quota 5× Pro oppure $200/mese con quota 20× Pro](https://blog.google/products-and-platforms/products/google-one/google-ai-subscriptions/).
Il problema è la misurabilità: Google dichiara che i limiti dipendono dalla capacità disponibile
e dalla quantità di lavoro compiuta dall'agente, possono cambiare e non corrispondono a un numero
pubblico fisso di richieste o token. Pro e Ultra possono acquistare crediti per continuare oltre
la quota base, con consumo ai prezzi della piattaforma Gemini.
Usando direttamente la Gemini API, Gemini 3.8 Flash costa fino al 31 dicembre 2026:
- **$0,75 per milione di token di input**;
- **$3,75 per milione di token di output**, inclusi i token di ragionamento;
- la metà in modalità Batch o Flex.
Dal 1º gennaio 2027 questi prezzi raddoppieranno a $1,50/$7,50. Esiste anche un free tier API,
con limiti, nel quale input e output sono gratuiti ma i contenuti possono essere usati per
migliorare i prodotti Google. Fonte: [pricing ufficiale Gemini API](https://ai.google.dev/gemini-api/docs/pricing).
## Qualità del modello
[Gemini 3.8 Flash](https://ai.google.dev/gemini-api/docs/models/gemini-3.8-flash) è GA, offre un
contesto da 1.048.576 token, output fino a 65.536 token, livelli di ragionamento low/medium/high e
strumenti per code execution, computer use, file search, function calling e search grounding.
I risultati pubblicati da Google lo collocano molto vicino ai modelli flagship su alcuni test di
coding, ma non su tutti:
| Benchmark | Gemini 3.8 Flash | Claude Opus 5 | GPT-5.6 Sol | Lettura onesta |
|---|---:|---:|---:|---|
| DeepSWE v1.1 | 73,7% | 74,0% | 72,7% | Prestazione quasi flagship sul software engineering end-to-end |
| Terminal-Bench 2.1 | 89,4% | 89,1% | 88,8% | Eccellente nel terminale sul benchmark più maturo |
| Terminal-Bench 4.0 | 19,1% | 51,8% | 37,3% | Forte calo sul test nuovo e più difficile: non è universalmente al livello dei flagship |
La [metodologia ufficiale Google](https://deepmind.google/models/evals-methodology/gemini-3-8-flash/)
precisa che diversi punteggi Gemini sono calcolati internamente e che i concorrenti provengono
anche da risultati auto-dichiarati; inoltre DeepSWE usa mini-swe, mentre Terminal-Bench usa un
harness diverso. I numeri vanno quindi letti come indicazione, non come garanzia. Una
[ricostruzione della tabella e dei confronti](https://www.vellum.ai/blog/gemini-3-8-flash-benchmarks-explained)
mostra lo stesso andamento: molto competitivo sui compiti di coding già ben rappresentati, più
debole su alcune prove nuove e aperte.
La conclusione qualitativa è: **ottimo implementatore quotidiano e subagente veloce**, con qualità
da modello molto più costoso in diversi task; per architettura difficile, debugging ambiguo o
lavori ad alto rischio è ancora sensato affiancargli un modello più forte come pianificatore o
revisore.
## Perché Antigravity è particolarmente adatto al server
La [CLI ufficiale Antigravity](https://antigravity.google/docs/cli/overview/) è una TUI interattiva
con editing multi-file, cronologia, tool calling, sandbox e subagenti. La
[guida d'installazione e autenticazione](https://antigravity.google/docs/cli/install/) conferma:
- esecuzione nativa su Linux, macOS e Windows;
- binario `agy` installato in `~/.local/bin` su Linux/macOS;
- quando rileva SSH, stampa un URL da aprire sul Mac e richiede di incollare nel terminale il
codice ottenuto;
- nessuna porta in ascolto e nessun port forwarding sono necessari per questo login.
Questo risolve meglio di ZCode il vincolo CyberArk, purché il server possa effettuare connessioni
HTTPS in uscita e sia consentita l'installazione del binario.
Per consumare la quota della registrazione Antigravity bisogna usare il client ufficiale. I
[termini Antigravity](https://antigravity.google/terms) vietano di riutilizzare il login/OAuth con
Pi, OMP, Claude Code, OpenCode o altri client. Con questi harness si può invece usare una normale
chiave Gemini API, pagando o consumando la quota API separata.
## Privacy e codice aziendale
Con un account personale Google registra le interazioni e può usarle per valutare e migliorare
prodotti e modelli; l'utente può disattivare l'uso dalle impostazioni. I termini Enterprise sono
diversi e la documentazione dichiara che codice, prompt e trascrizioni delle organizzazioni non
sono usati per addestrare i modelli Google. Fonti:
[termini Antigravity](https://antigravity.google/terms) e
[integrazioni Enterprise](https://antigravity.google/docs/ide/extensions/).
Su un server aziendale protetto da CyberArk userei quindi una registrazione personale solo dopo
aver verificato la policy interna; in caso contrario sceglierei l'accesso Antigravity Enterprise
tramite il progetto Google Cloud dell'organizzazione.
## Raccomandazione finale
1. Creare l'account gratuito e usare `agy` sul server per una settimana con task reali.
2. Tenere disabilitato l'uso automatico dei crediti extra e osservare i due indicatori di quota.
3. Passare a Pro soltanto se il limite gratuito interferisce con il lavoro.
4. Non acquistare Ultra finché Pro non viene saturato con regolarità.
5. Usare Gemini 3.8 Flash come modello principale per esplorazione, implementazione e test; per le
decisioni più difficili, mantenere Codex/Sol/Opus o un altro modello forte come revisore.
In breve: **sì, oggi Antigravity gratuito o Pro offre uno dei migliori rapporti costo/qualità per
Gemini 3.8 Flash**, e nel caso specifico la CLI ufficiale senza tunnel aumenta ulteriormente il
valore. Il limite commerciale da accettare è la quota non numerica e modificabile da Google.

Some files were not shown because too many files have changed in this diff Show More