ci / verify (push) Canceled after 0s
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.
186 lines
6.2 KiB
Go
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)}
|
|
}
|