Files
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
2026-10-08 10:06:44 +08:00

186 lines
6.2 KiB
Go

package vikingmemory
import (
"context"
"data-terminal-cloud/internal/callmemory"
"data-terminal-cloud/internal/outbox"
"encoding/json"
"errors"
"fmt"
"github.com/jackc/pgx/v5/pgxpool"
"strconv"
"strings"
)
const ProjectionAssistantID = "platform_companion_v2"
type ProjectionAdapter struct {
pool *pgxpool.Pool
client *Client
}
type projectionSchema struct {
EventType string `json:"event_type"`
ProfileType string `json:"profile_type"`
IDField string `json:"memory_id_field"`
VersionField string `json:"record_version_field"`
OperationField string `json:"operation_key_field"`
EventContent string `json:"event_content_field"`
ProfileContent string `json:"profile_content_field"`
ProfileGroups bool `json:"profile_groups_verified"`
}
func NewProjectionAdapter(pool *pgxpool.Pool, client *Client) *ProjectionAdapter {
return &ProjectionAdapter{pool, client}
}
func (a *ProjectionAdapter) schema(ctx context.Context) (projectionSchema, error) {
var p projectionSchema
var raw []byte
e := a.pool.QueryRow(ctx, `SELECT projection_schema FROM call_provider_profile WHERE published AND capabilities->>'H08'='true' AND COALESCE(evidence->>'H08','')<>'' ORDER BY created_at DESC LIMIT 1`).Scan(&raw)
if e != nil {
return p, &callmemory.Error{Status: 409, Code: "PROJECTION_SCHEMA_UNVERIFIED"}
}
if e = json.Unmarshal(raw, &p); e != nil {
return p, e
}
if p.EventType == "" || p.IDField == "" || p.VersionField == "" || p.OperationField == "" || p.EventContent == "" {
return p, &callmemory.Error{Status: 409, Code: "PROJECTION_SCHEMA_UNVERIFIED"}
}
return p, nil
}
// Project uses provider CRUD APIs, never AddSession. An uncertain add is only
// reconciled by its persistent operation key; absence in a query is not permission
// to create again. A full 5000-item result cannot prove absence and is blocked.
func (a *ProjectionAdapter) Project(ctx context.Context, r callmemory.ProjectionRecord, priorKind, priorID string, reconcileOnly bool) (callmemory.ProjectionResult, error) {
out := callmemory.ProjectionResult{}
p, e := a.schema(ctx)
if e != nil {
return out, e
}
kind := "event"
rule := p.EventType
field := p.EventContent
if r.Kind == "PROFILE" {
if p.ProfileType == "" || p.ProfileContent == "" || !p.ProfileGroups {
return out, &callmemory.Error{Status: 409, Code: "PROFILE_PROJECTION_UNVERIFIED"}
}
kind = "profile"
rule = p.ProfileType
field = p.ProfileContent
}
if priorID != "" && priorKind != "" && priorKind != kind {
return out, &callmemory.Error{Status: 409, Code: "PROJECTION_KIND_CHANGE_REQUIRES_CLEANUP"}
}
user := UserID(r.Tenant, r.Subject)
operation := r.ID + ":" + strconv.FormatInt(r.Version, 10)
serve := r.Status == "ACTIVE" && r.Policy == "ALLOWED" && (r.Kind != "OPEN_ITEM" || r.ItemStatus == "OPEN")
items, complete, e := a.list(ctx, kind, rule, user)
if e != nil {
return out, e
}
matches := []Item{}
for _, item := range items {
var id string
_ = json.Unmarshal(item.MemoryInfo[p.IDField], &id)
if id == r.ID {
matches = append(matches, item)
}
}
if len(matches) > 1 {
return out, &callmemory.Error{Status: 409, Code: "DUPLICATE_PROVIDER_PROJECTION"}
}
if len(matches) == 1 {
priorID = matches[0].ID
out.ID = priorID
out.Kind = kind
var version int64
_ = json.Unmarshal(matches[0].MemoryInfo[p.VersionField], &version)
var content string
_ = json.Unmarshal(matches[0].MemoryInfo[field], &content)
if serve && version == r.Version && content == r.Content {
out.Visible = true
return out, nil
}
}
if !serve {
if reconcileOnly && priorID == "" && len(matches) == 0 {
return out, errors.New("uncertain create must be reconciled before proving deletion")
}
if len(matches) == 0 && complete {
out.Kind = kind
out.ID = priorID
out.Visible = true
return out, nil
}
if priorID == "" {
return out, &callmemory.Error{Status: 409, Code: "PROVIDER_ABSENCE_NOT_PROVEN"}
}
e = a.client.call(ctx, "/api/memory/"+kind+"/delete", map[string]any{kind + "_id": priorID}, nil)
if e != nil {
return out, projectionError(e)
}
out.ID = priorID
out.Kind = kind
return out, nil
}
if reconcileOnly && priorID == "" {
return out, errors.New("provider add outcome remains unknown")
}
if !complete && priorID == "" {
return out, &callmemory.Error{Status: 409, Code: "PROVIDER_SCOPE_TOO_LARGE"}
}
info := map[string]any{p.IDField: r.ID, p.VersionField: r.Version, p.OperationField: operation, field: r.Content}
payload := map[string]any{"memory_info": info}
path := "/api/memory/" + kind + "/update"
if priorID != "" {
payload[kind+"_id"] = priorID
} else {
path = "/api/memory/" + kind + "/add"
payload[kind+"_type"] = rule
payload["user_id"] = user
payload["assistant_id"] = ProjectionAssistantID
if kind == "profile" {
payload["group_id"] = r.ID
payload["is_upsert"] = false
} else {
payload["update_profiles"] = []string{}
}
}
var data map[string]json.RawMessage
e = a.client.call(ctx, path, payload, &data)
if e != nil {
return out, projectionError(e)
}
if priorID == "" {
_ = json.Unmarshal(data[kind+"_id"], &priorID)
if strings.TrimSpace(priorID) == "" {
return out, errors.New("provider acknowledgement has no object identity")
}
}
out.Kind = kind
out.ID = priorID
out.Visible = false
return out, nil
}
func (a *ProjectionAdapter) list(ctx context.Context, kind, rule, user string) ([]Item, bool, error) {
var data struct {
Items []Item `json:"result_list"`
}
e := a.client.call(ctx, "/api/memory/"+kind+"/search", map[string]any{"filter": map[string]any{"user_id": []string{user}, "assistant_id": []string{ProjectionAssistantID}, "memory_type": []string{rule}}, "limit": 5000}, &data)
if e != nil {
return nil, false, projectionError(e)
}
for _, item := range data.Items {
if item.ID == "" || !contains(item.UserID, user) || !contains(item.AssistantID, ProjectionAssistantID) {
return nil, false, &callmemory.Error{Status: 409, Code: "PROVIDER_SCOPE_VIOLATION"}
}
}
return data.Items, len(data.Items) < 5000, nil
}
func projectionError(e error) error {
if outbox.IsRetryable(e) {
return e
}
return &callmemory.Error{Status: 409, Code: "PROVIDER_SCHEMA_OR_AUTH_REJECTED", Message: fmt.Sprintf("Provider rejected projection: %T", e)}
}