diff --git a/components/pandora/ShadowContextPackControls.tsx b/components/pandora/ShadowContextPackControls.tsx
new file mode 100644
index 0000000..66e3d22
--- /dev/null
+++ b/components/pandora/ShadowContextPackControls.tsx
@@ -0,0 +1,4 @@
+"use client";
+import type { ShadowContextPackSummary } from "./types";
+async function post(id: string, verb: "review" | "reject" | "archive") { await fetch(`/api/pandora/shadow-context-packs/${id}/${verb}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) }); window.location.reload(); }
+export function ShadowContextPackControls({ pack }: { pack: ShadowContextPackSummary }) { return
post(pack.id, "review")}>Mark reviewed post(pack.id, "reject")}>Reject post(pack.id, "archive")}>Archive
; }
diff --git a/components/pandora/ShadowContextPackDetail.tsx b/components/pandora/ShadowContextPackDetail.tsx
new file mode 100644
index 0000000..2d3e66c
--- /dev/null
+++ b/components/pandora/ShadowContextPackDetail.tsx
@@ -0,0 +1,4 @@
+import type { ShadowContextPackSummary } from "./types";
+import { ShadowContextPackEvidence } from "./ShadowContextPackEvidence";
+import { ShadowContextPackControls } from "./ShadowContextPackControls";
+export function ShadowContextPackDetail({ pack }: { pack: ShadowContextPackSummary }) { const payload = pack.candidate_payload; return
{pack.title} {pack.summary}
{pack.status} {pack.namespace} namespace
{String(payload.source_event_count ?? 0)} source events
{String(payload.active_master_pack_id ?? "none")} active master pack
{String(payload.latest_event_at ?? "none")} latest event
Candidate payload {JSON.stringify(payload, null, 2)} ; }
diff --git a/components/pandora/ShadowContextPackEvidence.tsx b/components/pandora/ShadowContextPackEvidence.tsx
new file mode 100644
index 0000000..a6655f0
--- /dev/null
+++ b/components/pandora/ShadowContextPackEvidence.tsx
@@ -0,0 +1,2 @@
+import type { ShadowContextPackSummary } from "./types";
+export function ShadowContextPackEvidence({ pack }: { pack: ShadowContextPackSummary }) { return
Evidence summary: {String(pack.candidate_payload.evidence_summary ?? pack.summary)}
{pack.warnings.map((w) =>
⚠ {w}
)}
; }
diff --git a/components/pandora/ShadowContextPackLabCard.tsx b/components/pandora/ShadowContextPackLabCard.tsx
new file mode 100644
index 0000000..32ceef3
--- /dev/null
+++ b/components/pandora/ShadowContextPackLabCard.tsx
@@ -0,0 +1,4 @@
+import type { ShadowContextPackLabData } from "./types";
+import { ShadowContextPackList } from "./ShadowContextPackList";
+const empty: ShadowContextPackLabData = { packs: [], warnings: [], countsByStatus: { draft: 0, ready_for_review: 0, reviewed: 0, rejected: 0, archived: 0 } };
+export function ShadowContextPackLabCard({ data }: { data?: ShadowContextPackLabData }) { const lab = data ?? empty; return
Shadow Context Pack Lab
Staged candidates for operator review Shadow only — no production memory mutation
Promotion unavailable {Object.entries(lab.countsByStatus).map(([status, count]) =>
{count} {status}
)}
{lab.warnings.length ? {lab.warnings.map((w) =>
⚠ {w}
)}
: null}; }
diff --git a/components/pandora/ShadowContextPackList.tsx b/components/pandora/ShadowContextPackList.tsx
new file mode 100644
index 0000000..f6a5928
--- /dev/null
+++ b/components/pandora/ShadowContextPackList.tsx
@@ -0,0 +1,3 @@
+import type { ShadowContextPackSummary } from "./types";
+import { ShadowContextPackDetail } from "./ShadowContextPackDetail";
+export function ShadowContextPackList({ packs }: { packs: ShadowContextPackSummary[] }) { if (!packs.length) return
No shadow context-pack candidates staged yet.
; return
{packs.map((pack) => )}
; }
diff --git a/components/pandora/mock-data.ts b/components/pandora/mock-data.ts
index 3fc1aa3..f111a75 100644
--- a/components/pandora/mock-data.ts
+++ b/components/pandora/mock-data.ts
@@ -17,5 +17,5 @@ export const timelineEvents: TimelineEventData[] = [{ id: "fixture", color: "sla
export const coreSystems: SystemRow[] = [{ label: "Fixture", value: "No live data", state: "idle" }];
export const gatedSystems: SystemRow[] = [{ label: "Semantic retrieval", value: "Gated", state: "gated" }];
const verification = { generatedAt: "No live data", status: "not_run" as const, namespaces: [], packSupersession: { status: "not_run" as const, namespaces: [], warnings: ["Mock only"] }, retrievalEval: { status: "not_run" as const, source: "fixture", latestRunId: null, latestRunAt: null, resultLabel: "Not run", realResultAvailable: false, warnings: ["Mock only"] }, auditEvidence: [], smokeEvidence: { status: "not_run" as const, latest: null, warnings: ["Mock only"] }, invariantStatus: { exactlyOneActiveMasterPerNamespace: "not_run" as const, noCrossNamespacePackMixing: "not_run" as const, noDuplicateActiveMaster: "not_run" as const, retrievalEvalHasNoFabricatedScore: "pass" as const, smokeEvidence: "not_run" as const }, warnings: ["Mock only"] };
-export const fixtureDashboardData: PandoraDashboardData = { generatedAt: "No live data", operatorLabel: "Fixture", live: false, warnings: ["Mock only"], hero: { title: "Fixture dashboard", description: "Mock only: no live data.", primaryAction: "No live data", secondaryAction: "Semantic gated" }, evidence: "No live data", stats: [{ id: "fixture", title: "Fixture", value: "No live data", subtitle: "Mock only", color: "slate", sparklineData: [0, 0] }], memorySpaces, workQueue, profileSnapshot, timelineEvents, diagnostics: { coreSystems, gatedSystems, envelope: { title: "Fixture", description: "Mock only: no live data." } }, verification, operatorActions: { actions: [], warnings: [], countsByStatus: { proposed: 0, dry_ran: 0, approved: 0, executing: 0, completed: 0, blocked: 0, failed: 0, cancelled: 0 } } };
+export const fixtureDashboardData: PandoraDashboardData = { generatedAt: "No live data", operatorLabel: "Fixture", live: false, warnings: ["Mock only"], hero: { title: "Fixture dashboard", description: "Mock only: no live data.", primaryAction: "No live data", secondaryAction: "Semantic gated" }, evidence: "No live data", stats: [{ id: "fixture", title: "Fixture", value: "No live data", subtitle: "Mock only", color: "slate", sparklineData: [0, 0] }], memorySpaces, workQueue, profileSnapshot, timelineEvents, diagnostics: { coreSystems, gatedSystems, envelope: { title: "Fixture", description: "Mock only: no live data." } }, verification, shadowContextPackLab: { packs: [], warnings: [], countsByStatus: { draft: 0, ready_for_review: 0, reviewed: 0, rejected: 0, archived: 0 } }, operatorActions: { actions: [], warnings: [], countsByStatus: { proposed: 0, dry_ran: 0, approved: 0, executing: 0, completed: 0, blocked: 0, failed: 0, cancelled: 0 } } };
diff --git a/components/pandora/types.ts b/components/pandora/types.ts
index 0f825e4..2714221 100644
--- a/components/pandora/types.ts
+++ b/components/pandora/types.ts
@@ -142,8 +142,13 @@ export type PandoraVerificationData = {
};
+export type ShadowContextPackStatus = "draft" | "ready_for_review" | "reviewed" | "rejected" | "archived";
+export type ShadowContextPackEvent = { id: string; shadow_pack_id: string; user_id: string; event_type: string; message: string; metadata: Record
; created_at: string };
+export type ShadowContextPackSummary = { id: string; request_id: string; operator_action_id?: string | null; namespace: PandoraNamespace; pack_type: string; status: ShadowContextPackStatus; title: string; summary: string; source_window: Record; candidate_payload: Record; evidence: Record; warnings: string[]; created_at: string; updated_at: string; reviewed_at?: string | null; rejected_at?: string | null; archived_at?: string | null; event_preview?: ShadowContextPackEvent[] };
+export type ShadowContextPackLabData = { packs: ShadowContextPackSummary[]; warnings: string[]; countsByStatus: Record };
+
export type OperatorActionStatus = "proposed" | "dry_ran" | "approved" | "executing" | "completed" | "blocked" | "failed" | "cancelled";
-export type OperatorActionType = "verify_namespace_invariants" | "verify_pack_supersession" | "check_retrieval_eval_status" | "refresh_dashboard_snapshot" | "prepare_distill_smoke_plan";
+export type OperatorActionType = "verify_namespace_invariants" | "verify_pack_supersession" | "check_retrieval_eval_status" | "refresh_dashboard_snapshot" | "prepare_distill_smoke_plan" | "prepare_shadow_context_pack";
export type OperatorActionMode = "dry_run" | "queued_only";
export type OperatorActionEventSummary = { id: string; action_id: string; user_id: string; event_type: string; message: string; metadata: Record; created_at: string; };
@@ -202,4 +207,5 @@ export type PandoraDashboardData = {
};
verification: PandoraVerificationData;
operatorActions: OperatorActionCenterData;
+ shadowContextPackLab: ShadowContextPackLabData;
};
diff --git a/docs/pandora-operator-action-center.md b/docs/pandora-operator-action-center.md
index 62f3b87..30dfcb4 100644
--- a/docs/pandora-operator-action-center.md
+++ b/docs/pandora-operator-action-center.md
@@ -45,3 +45,7 @@ Approved actions execute verification loaders only. Results include `no_mutation
## Idempotency and events
The service hashes the server-derived `userId`, action type, namespace, normalized payload, and mode. The database enforces `unique(user_id, idempotency_key)`. Every proposed, dry-run, approved, executing, completed, failed, and cancelled transition writes an event row.
+
+## Shadow context-pack action
+
+`prepare_shadow_context_pack` is available as a dry-run/queued-only operator action. Dry-run computes deterministic shadow candidate evidence without inserting a row. Approved execution writes only to the new shadow context-pack tables plus operator action events. It does not mutate core memory tables and does not promote to `memory_context_packs`.
diff --git a/docs/pandora-readonly-action-runner.md b/docs/pandora-readonly-action-runner.md
index c4399ea..e421a53 100644
--- a/docs/pandora-readonly-action-runner.md
+++ b/docs/pandora-readonly-action-runner.md
@@ -61,3 +61,7 @@ Each state transition creates an event row with user scope, action id, event typ
## Future path to human-approved live actions
Any future live action needs a separate reviewed PR, protected dry-run output, explicit human approval, production readiness checks, database verification, and post-run audit evidence. This runner is not that live-mutation path.
+
+## Shadow staging exception
+
+The existing verification actions remain read-only. `prepare_shadow_context_pack` is the only staged write exception in this phase: after approval/execution it inserts a shadow candidate in `pandora_shadow_context_packs` and a matching shadow event. This is not a production context-pack promotion and cannot update `memory_context_packs`.
diff --git a/docs/pandora-shadow-context-pack-lab.md b/docs/pandora-shadow-context-pack-lab.md
new file mode 100644
index 0000000..96b62fd
--- /dev/null
+++ b/docs/pandora-shadow-context-pack-lab.md
@@ -0,0 +1,37 @@
+# Pandora Shadow Context Pack Lab v1
+
+Shadow context packs are staged, operator-review candidates stored outside `memory_context_packs`. They let Pandora prepare deterministic context-pack evidence without replacing, promoting, deleting, distilling, embedding, or mutating production memory truth tables.
+
+## Safety boundary
+
+- Writes are limited to `pandora_shadow_context_packs`, `pandora_shadow_context_pack_events`, and the existing operator action/event tables.
+- `memory_events`, `memory_context_packs`, `memory_profiles`, `memory_capture_candidates`, and `memory_pruning_candidates` are not mutated.
+- No model calls, embeddings, semantic retrieval, GPT Actions, or MCP are used.
+- Promotion to real context packs is unavailable in v1.
+- All reads and writes use server-derived authenticated user identity and namespace filters.
+
+## Statuses
+
+Shadow packs may be `draft`, `ready_for_review`, `reviewed`, `rejected`, or `archived`.
+
+## Candidate generation
+
+The service reads recent namespace-scoped `memory_events`, active master pack metadata, open loops, and review queue counts. It produces deterministic payload fields: source event count, active master id, latest event timestamp, reviewed/promoted counts, open-loop count, review count, short recent summaries, and an evidence summary. Missing events or active master metadata create warnings and keep the candidate in `draft`.
+
+## Operator action flow
+
+`prepare_shadow_context_pack` can be proposed from the Operator Action Center. Dry-run computes the staged candidate and performs no insert. After approval, execution creates exactly one shadow candidate and shadow event; result metadata states `shadow_write_performed: true`, `no_core_memory_mutation_performed: true`, and `no_promotion_performed: true`.
+
+## Manual smoke checklist
+
+1. Sign in to `/pandora`.
+2. Propose `prepare_shadow_context_pack` for one namespace.
+3. Run dry-run and verify no shadow row was inserted.
+4. Approve and execute.
+5. Confirm the new candidate appears in the Shadow Context Pack Lab.
+6. Mark reviewed, reject, or archive the candidate.
+7. Confirm no promote, replace, delete, prune, merge, or distill controls exist.
+
+## Future path
+
+A future human-approved promotion path would require a separate PR, explicit review, additional migration/policy review, dry-run proof, and production approval. It is intentionally not implemented here.
diff --git a/lib/services/pandora-dashboard-service.ts b/lib/services/pandora-dashboard-service.ts
index 39f1798..b0adcd5 100644
--- a/lib/services/pandora-dashboard-service.ts
+++ b/lib/services/pandora-dashboard-service.ts
@@ -2,6 +2,7 @@
import type { PandoraDashboardData } from "@/components/pandora/types";
import { loadPandoraVerificationData } from "@/lib/services/pandora-verification-service";
import { listOperatorActions } from "@/lib/services/pandora-operator-action-service";
+import { listShadowContextPacks } from "@/lib/services/pandora-shadow-context-pack-service";
export type PandoraDashboardDbClient = { from: (table: string) => any };
type Namespace = "real_life" | "au";
@@ -41,6 +42,9 @@ export async function loadPandoraDashboardData(client: PandoraDashboardDbClient,
const verification = await loadPandoraVerificationData(client, { userId: input.userId });
const actionWarnings: string[] = [];
const operatorActions = await listOperatorActions(client, { userId: input.userId, limit: 10 });
+ const shadowWarnings: string[] = [];
+ let shadowPacks = [] as Row[];
+ try { shadowPacks = await listShadowContextPacks(client, { userId: input.userId, limit: 8 }) as Row[]; } catch { shadowWarnings.push("shadow context-pack tables unavailable; lab is showing an empty state."); }
const actionEvents = await Promise.all(operatorActions.map(async (action) => {
try {
const result = await client.from("pandora_operator_action_events").select("*").eq("user_id", input.userId).eq("action_id", action.id).order("created_at", { ascending: false }).limit(3);
@@ -52,6 +56,8 @@ export async function loadPandoraDashboardData(client: PandoraDashboardDbClient,
}
}));
const eventsByAction = new Map(actionEvents.map((item) => [item.actionId, item.events]));
+ const shadowStatuses = ["draft", "ready_for_review", "reviewed", "rejected", "archived"] as const;
+ const shadowCountsByStatus = Object.fromEntries(shadowStatuses.map((status) => [status, shadowPacks.filter((pack) => pack.status === status).length])) as PandoraDashboardData["shadowContextPackLab"]["countsByStatus"];
const actionStatuses = ["proposed", "dry_ran", "approved", "executing", "completed", "blocked", "failed", "cancelled"] as const;
const countsByStatus = Object.fromEntries(actionStatuses.map((status) => [status, operatorActions.filter((action) => action.status === status).length])) as PandoraDashboardData["operatorActions"]["countsByStatus"];
const data = await Promise.all(namespaces.map(async (namespace) => ({
@@ -79,7 +85,7 @@ export async function loadPandoraDashboardData(client: PandoraDashboardDbClient,
generatedAt: new Date().toISOString(),
operatorLabel: input.operatorLabel ?? input.userId,
live: warnings.length === 0,
- warnings: Array.from(new Set([...warnings, ...verification.warnings])),
+ warnings: Array.from(new Set([...warnings, ...verification.warnings, ...shadowWarnings])),
hero: { title: "Pandora dashboard is reading live memory state.", description: "This route renders authenticated Supabase data for memory state while semantic retrieval, embeddings, model calls, GPT Actions, and MCP remain gated.", primaryAction: "Context pack data live", secondaryAction: "Retrieval eval Gated" },
evidence: warnings.length ? "Live dashboard read completed with safe empty states for unavailable tables." : "Live dashboard read complete from server-derived session scope.",
stats: [
@@ -101,6 +107,7 @@ export async function loadPandoraDashboardData(client: PandoraDashboardDbClient,
timelineEvents: events.slice(0, 6).map((event) => ({ id: String(event.id ?? `${event.namespace}-${event.created_at ?? "event"}`), title: `${event.namespace} • ${event.status ?? "unknown"}`, time: event.created_at ?? "Live read", desc: eventSummary(event), namespace: event.namespace === "au" ? "au" : "real_life", color: event.namespace === "au" ? "purple" : "emerald" })),
diagnostics: { coreSystems: [{ label: "Route exposure", value: "Auth gated", state: "healthy" }, { label: "Displayed data", value: warnings.length ? "Partial live reads" : "Live reads", state: warnings.length ? "attention" : "healthy" }, { label: "Master-pack invariant", value: duplicates ? `${duplicates} duplicate` : "OK", state: duplicates ? "attention" : "healthy" }, { label: "Client user_id", value: "Rejected", state: "healthy" }], gatedSystems: [{ label: "Semantic retrieval", value: "Gated Off", state: "gated" }, { label: "Embeddings", value: "Gated Off", state: "gated" }, { label: "Model calls", value: "Gated Off", state: "gated" }, { label: "Pruning automation", value: "Review-only", state: "gated" }], envelope: { title: "Dashboard Truth Envelope", description: warnings.length ? "Unavailable reads were converted to warnings and empty UI state." : "Live loader completed from authenticated Supabase reads." } },
verification,
+ shadowContextPackLab: { packs: shadowPacks.map((pack) => ({ id: String(pack.id), request_id: String(pack.request_id), operator_action_id: pack.operator_action_id ? String(pack.operator_action_id) : null, namespace: pack.namespace === "au" ? "au" : "real_life", pack_type: String(pack.pack_type ?? "master_candidate"), status: String(pack.status ?? "draft") as never, title: String(pack.title ?? "Shadow context pack"), summary: String(pack.summary ?? ""), source_window: pack.source_window ?? {}, candidate_payload: pack.candidate_payload ?? {}, evidence: pack.evidence ?? {}, warnings: Array.isArray(pack.warnings) ? pack.warnings : [], created_at: String(pack.created_at ?? ""), updated_at: String(pack.updated_at ?? ""), reviewed_at: pack.reviewed_at ?? null, rejected_at: pack.rejected_at ?? null, archived_at: pack.archived_at ?? null })), warnings: shadowWarnings, countsByStatus: shadowCountsByStatus },
operatorActions: { actions: operatorActions.map((action) => {
const eventPreview = eventsByAction.get(action.id) ?? [];
return { id: action.id, request_id: action.request_id, idempotency_key: action.idempotency_key, action_type: action.action_type, namespace: action.namespace, mode: action.mode, status: action.status, title: action.title, description: action.description, result: action.result, warnings: action.warnings, created_at: action.created_at, updated_at: action.updated_at, approved_at: action.approved_at, completed_at: action.completed_at, failed_at: action.failed_at, event_count: eventPreview.length, event_preview: eventPreview.map((event: Row) => ({ id: String(event.id), action_id: String(event.action_id), user_id: String(event.user_id), event_type: String(event.event_type), message: String(event.message), metadata: event.metadata ?? {}, created_at: String(event.created_at) })) };
diff --git a/lib/services/pandora-operator-action-service.ts b/lib/services/pandora-operator-action-service.ts
index d4ada23..a11d6a8 100644
--- a/lib/services/pandora-operator-action-service.ts
+++ b/lib/services/pandora-operator-action-service.ts
@@ -3,15 +3,16 @@ import { createHash, randomUUID } from "node:crypto";
import type { PandoraNamespace } from "@/components/pandora/types";
import { loadPandoraDashboardData } from "@/lib/services/pandora-dashboard-service";
import { loadPandoraVerificationData } from "@/lib/services/pandora-verification-service";
+import { createShadowContextPackCandidate, dryRunShadowContextPackCandidate } from "@/lib/services/pandora-shadow-context-pack-service";
-export type OperatorActionType = "verify_namespace_invariants" | "verify_pack_supersession" | "check_retrieval_eval_status" | "refresh_dashboard_snapshot" | "prepare_distill_smoke_plan";
+export type OperatorActionType = "verify_namespace_invariants" | "verify_pack_supersession" | "check_retrieval_eval_status" | "refresh_dashboard_snapshot" | "prepare_distill_smoke_plan" | "prepare_shadow_context_pack";
export type OperatorActionMode = "dry_run" | "queued_only";
export type OperatorActionStatus = "proposed" | "dry_ran" | "approved" | "executing" | "completed" | "blocked" | "failed" | "cancelled";
export type OperatorActionRow = { id: string; user_id: string; request_id: string; idempotency_key: string; action_type: OperatorActionType; namespace: PandoraNamespace | null; mode: OperatorActionMode; status: OperatorActionStatus; title: string; description: string; payload: Record; result: Record; warnings: string[]; created_at: string; updated_at: string; approved_at?: string | null; completed_at?: string | null; failed_at?: string | null };
export type OperatorActionEventRow = { id: string; action_id: string; user_id: string; event_type: string; message: string; metadata: Record; created_at: string };
export type OperatorActionDbClient = { from: (table: string) => any };
-const ACTIONS = new Set(["verify_namespace_invariants", "verify_pack_supersession", "check_retrieval_eval_status", "refresh_dashboard_snapshot", "prepare_distill_smoke_plan"]);
+const ACTIONS = new Set(["verify_namespace_invariants", "verify_pack_supersession", "check_retrieval_eval_status", "refresh_dashboard_snapshot", "prepare_distill_smoke_plan", "prepare_shadow_context_pack"]);
const MODES = new Set(["dry_run", "queued_only"]);
const NAMESPACES = new Set(["real_life", "au"]);
@@ -21,7 +22,7 @@ function assertNamespace(namespace?: string | null): asserts namespace is Pandor
function stable(value: unknown): string { if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`; if (value && typeof value === "object") return `{${Object.keys(value as Record).sort().map((k) => `${JSON.stringify(k)}:${stable((value as Record)[k])}`).join(",")}}`; return JSON.stringify(value); }
export function operatorActionIdempotencyKey(input: { userId: string; actionType: string; namespace?: string | null; payload?: unknown; mode: string }) { return createHash("sha256").update([input.userId, input.actionType, input.namespace ?? "global", input.mode, stable(input.payload ?? {})].join("|")).digest("hex"); }
function titleFor(actionType: OperatorActionType) { return actionType.split("_").map((p) => p[0].toUpperCase() + p.slice(1)).join(" "); }
-function envelope(action: OperatorActionRow, result: Record, warnings: string[]) { return { ok: warnings.length === 0, request_id: action.request_id, action_id: action.id, action_type: action.action_type, status: action.status, evidence_summary: result, warnings, no_mutation_performed: true, executed_at: new Date().toISOString() }; }
+function envelope(action: OperatorActionRow, result: Record, warnings: string[]) { return { ok: warnings.length === 0, request_id: action.request_id, action_id: action.id, action_type: action.action_type, status: action.status, evidence_summary: result, warnings, no_mutation_performed: result.shadow_write_performed === true ? false : true, no_core_memory_mutation_performed: true, no_promotion_performed: true, executed_at: new Date().toISOString() }; }
const TERMINAL = new Set(["completed", "failed", "cancelled"]);
function assertTransition(from: OperatorActionStatus, to: OperatorActionStatus) { const allowed: Record = { proposed: ["dry_ran", "approved", "cancelled"], dry_ran: ["approved", "cancelled"], approved: ["executing", "cancelled"], executing: ["completed", "failed"], blocked: [], completed: [], failed: [], cancelled: [] }; if (!allowed[from]?.includes(to)) throw new Error(`Invalid Pandora operator action transition: ${from} -> ${to}`); }
async function single(query: any) { const res = await query; if (res.error) return null; return Array.isArray(res.data) ? res.data[0] ?? null : res.data ?? null; }
@@ -65,6 +66,11 @@ export async function proposeOperatorAction(client: OperatorActionDbClient, inpu
async function buildDryRunResult(client: OperatorActionDbClient, userId: string, action: OperatorActionRow) {
const warnings: string[] = [];
+ if (action.action_type === "prepare_shadow_context_pack") {
+ if (!action.namespace) throw new Error("prepare_shadow_context_pack requires a namespace");
+ const candidate = await dryRunShadowContextPackCandidate(client, { userId, namespace: action.namespace, sourceWindow: action.payload?.source_window as Record | undefined });
+ return { warnings: candidate.warnings, result: { checked: "shadow_context_pack_candidate", ...candidate, no_core_memory_mutation_performed: true, shadow_write_performed: false, no_promotion_performed: true } };
+ }
const verification = await loadPandoraVerificationData(client, { userId });
if (action.action_type === "refresh_dashboard_snapshot") { const dashboard = await loadPandoraDashboardData(client, { userId }); warnings.push(...dashboard.warnings); return { warnings, result: { checked: "dashboard_snapshot", generated_at: dashboard.generatedAt, memory_spaces: dashboard.memorySpaces.map((s) => ({ namespace: s.id, memories: s.memories, status: s.status })), gated_systems: dashboard.diagnostics.gatedSystems, no_mutation_performed: true } }; }
warnings.push(...verification.warnings);
@@ -81,7 +87,7 @@ export async function dryRunOperatorAction(client: OperatorActionDbClient, input
const status: OperatorActionStatus = "dry_ran";
const nextResult = envelope({ ...action, status }, built.result, built.warnings);
const updated = await single(client.from("pandora_operator_actions").update({ status, result: nextResult, warnings: built.warnings, updated_at: new Date().toISOString() }).eq("user_id", input.userId).eq("id", input.actionId).select("*").single());
- await createActionEvent(client, { userId: input.userId, actionId: input.actionId, eventType: status, message: "Safe dry-run completed; no core memory mutation was performed.", metadata: nextResult });
+ await createActionEvent(client, { userId: input.userId, actionId: input.actionId, eventType: action.action_type === "prepare_shadow_context_pack" ? "shadow_candidate_dry_run" : status, message: action.action_type === "prepare_shadow_context_pack" ? "Shadow context-pack candidate dry-run completed; no shadow row or core memory mutation was performed." : "Safe dry-run completed; no core memory mutation was performed.", metadata: nextResult });
return updated ?? { ...action, status, result: nextResult, warnings: built.warnings };
}
@@ -103,9 +109,15 @@ export async function executeApprovedOperatorAction(client: OperatorActionDbClie
assertMode(action.mode);
const executingAt = new Date().toISOString();
const executing = await single(client.from("pandora_operator_actions").update({ status: "executing", updated_at: executingAt }).eq("user_id", input.userId).eq("id", input.actionId).select("*").single()) ?? { ...action, status: "executing" as const, updated_at: executingAt };
- await createActionEvent(client, { userId: input.userId, actionId: input.actionId, eventType: "executing", message: "Approved read-only verification execution started.", metadata: { no_mutation_performed: true } });
+ await createActionEvent(client, { userId: input.userId, actionId: input.actionId, eventType: "executing", message: action.action_type === "prepare_shadow_context_pack" ? "Approved shadow staging execution started." : "Approved read-only verification execution started.", metadata: { no_mutation_performed: action.action_type !== "prepare_shadow_context_pack", no_core_memory_mutation_performed: true } });
try {
- const built = await buildDryRunResult(client, input.userId, executing);
+ let built: { warnings: string[]; result: Record } = await buildDryRunResult(client, input.userId, executing);
+ if (executing.action_type === "prepare_shadow_context_pack") {
+ if (!executing.namespace) throw new Error("prepare_shadow_context_pack requires a namespace");
+ const shadow = await createShadowContextPackCandidate(client, { userId: input.userId, namespace: executing.namespace, operatorActionId: executing.id, requestId: executing.request_id, sourceWindow: executing.payload?.source_window as Record | undefined });
+ built = { warnings: shadow.warnings, result: { checked: "shadow_context_pack_candidate", shadow_pack_id: shadow.id, status: shadow.status, namespace: shadow.namespace, shadow_write_performed: true, no_core_memory_mutation_performed: true, no_promotion_performed: true } };
+ await createActionEvent(client, { userId: input.userId, actionId: input.actionId, eventType: "shadow_candidate_created", message: "Shadow context-pack candidate created in staging tables only.", metadata: built.result });
+ }
const completedAt = new Date().toISOString();
const result = envelope({ ...executing, status: "completed" }, built.result, built.warnings);
const completed = await single(client.from("pandora_operator_actions").update({ status: "completed", result, warnings: built.warnings, completed_at: completedAt, updated_at: completedAt }).eq("user_id", input.userId).eq("id", input.actionId).select("*").single());
diff --git a/lib/services/pandora-shadow-context-pack-service.ts b/lib/services/pandora-shadow-context-pack-service.ts
new file mode 100644
index 0000000..37c565e
--- /dev/null
+++ b/lib/services/pandora-shadow-context-pack-service.ts
@@ -0,0 +1,25 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { randomUUID } from "node:crypto";
+import type { PandoraNamespace, ShadowContextPackStatus } from "@/components/pandora/types";
+
+export type ShadowContextPackDbClient = { from: (table: string) => any };
+export type ShadowContextPackRow = { id: string; user_id: string; request_id: string; operator_action_id?: string | null; namespace: PandoraNamespace; pack_type: string; status: ShadowContextPackStatus; title: string; summary: string; source_window: Record; candidate_payload: Record; evidence: Record; warnings: string[]; created_at: string; updated_at: string; reviewed_at?: string | null; rejected_at?: string | null; archived_at?: string | null };
+export type ShadowContextPackEventRow = { id: string; shadow_pack_id: string; user_id: string; event_type: string; message: string; metadata: Record; created_at: string };
+const NAMESPACES = new Set(["real_life", "au"]); const STATUSES = new Set(["draft", "ready_for_review", "reviewed", "rejected", "archived"]);
+function assertNamespace(namespace: string): asserts namespace is PandoraNamespace { if (!NAMESPACES.has(namespace)) throw new Error(`Unsupported Pandora namespace: ${namespace}`); }
+function assertStatus(status: string): asserts status is ShadowContextPackStatus { if (!STATUSES.has(status)) throw new Error(`Unsupported shadow context pack status: ${status}`); }
+async function safeRows(client: ShadowContextPackDbClient, table: string, userId: string, namespace: PandoraNamespace, limit: number, warnings: string[]) { try { const r = await client.from(table).select("*").eq("user_id", userId).eq("namespace", namespace).order("created_at", { ascending: false }).limit(limit); if (r.error) { warnings.push(`${table} unavailable for ${namespace}`); return []; } return Array.isArray(r.data) ? r.data : []; } catch { warnings.push(`${table} unavailable for ${namespace}`); return []; } }
+function short(value: unknown) { return String(value ?? "").replace(/\s+/g, " ").trim().slice(0, 160); }
+function eventSummary(row: any) { return short(row.extracted_summary || row.summary || row.raw_text || row.title || row.id); }
+async function single(query: any) { const res = await query; if (res.error) return null; return Array.isArray(res.data) ? res.data[0] ?? null : res.data ?? null; }
+
+export async function listShadowContextPacks(client: ShadowContextPackDbClient, input: { userId: string; namespace?: string; status?: string; limit?: number }): Promise { if (input.namespace) assertNamespace(input.namespace); if (input.status) assertStatus(input.status); let q = client.from("pandora_shadow_context_packs").select("*").eq("user_id", input.userId).order("created_at", { ascending: false }).limit(input.limit ?? 20); if (input.namespace) q = q.eq("namespace", input.namespace); if (input.status) q = q.eq("status", input.status); const r = await q; if (r.error) return []; return Array.isArray(r.data) ? r.data : []; }
+export async function getShadowContextPack(client: ShadowContextPackDbClient, input: { userId: string; shadowPackId: string }): Promise { const row = await single(client.from("pandora_shadow_context_packs").select("*").eq("user_id", input.userId).eq("id", input.shadowPackId).limit(1)); if (!row) throw new Error("Shadow context pack not found for current user"); return row; }
+
+export async function dryRunShadowContextPackCandidate(client: ShadowContextPackDbClient, input: { userId: string; namespace: string; sourceWindow?: Record }) { assertNamespace(input.namespace); const warnings: string[] = []; const events = await safeRows(client, "memory_events", input.userId, input.namespace, 50, warnings); const packs = await safeRows(client, "memory_context_packs", input.userId, input.namespace, 10, warnings); const loops = await safeRows(client, "memory_open_loops", input.userId, input.namespace, 100, warnings); const review = await safeRows(client, "memory_review_queue_items", input.userId, input.namespace, 100, warnings); const active = packs.find((p:any)=>p.pack_type === "master" && p.status === "active"); const reviewed = events.filter((e:any)=>e.status === "reviewed").length; const promoted = events.filter((e:any)=>e.status === "promoted").length; const openLoopCount = loops.filter((l:any)=>["open","acknowledged"].includes(String(l.status ?? ""))).length; const needsReviewCount = review.filter((r:any)=>["pending_review","needs_clarification","pending"].includes(String(r.status ?? ""))).length; if (!active) warnings.push(`No active master context pack found for ${input.namespace}; readiness is not fabricated.`); if (events.length === 0) warnings.push(`No memory_events found for ${input.namespace}; candidate remains draft.`); const latest = events[0]?.created_at ?? null; const payload = { namespace: input.namespace, source_event_count: events.length, active_master_pack_id: active?.id ?? null, active_master_created_at: active?.created_at ?? null, latest_event_at: latest, reviewed_event_count: reviewed, promoted_event_count: promoted, open_loop_count: openLoopCount, needs_review_count: needsReviewCount, top_recent_summaries: events.slice(0,5).map(eventSummary), evidence_summary: `${events.length} namespace-scoped memory_events, ${openLoopCount} open loops, ${needsReviewCount} review items; no semantic retrieval or model scoring.` };
+ const status: ShadowContextPackStatus = warnings.length || !active || events.length === 0 ? "draft" : "ready_for_review";
+ return { status, title: `Shadow ${input.namespace} master candidate`, summary: `Shadow candidate for ${input.namespace}: ${events.length} events considered, ${openLoopCount} open loops, ${needsReviewCount} review items. Promotion is gated.`, source_window: input.sourceWindow ?? {}, candidate_payload: payload, evidence: { namespace: input.namespace, source_tables: ["memory_events", "memory_context_packs", "memory_open_loops", "memory_review_queue_items"], no_model_call: true, no_embeddings: true, no_promotion_performed: true }, warnings };
+}
+export async function createShadowContextPackEvent(client: ShadowContextPackDbClient, input: { userId: string; shadowPackId: string; eventType: string; message: string; metadata?: Record }): Promise { const row = { id: randomUUID(), shadow_pack_id: input.shadowPackId, user_id: input.userId, event_type: input.eventType, message: input.message, metadata: input.metadata ?? {}, created_at: new Date().toISOString() }; return single(client.from("pandora_shadow_context_pack_events").insert(row).select("*").single()); }
+export async function createShadowContextPackCandidate(client: ShadowContextPackDbClient, input: { userId: string; namespace: string; operatorActionId?: string | null; requestId: string; sourceWindow?: Record }): Promise { const c = await dryRunShadowContextPackCandidate(client, { userId: input.userId, namespace: input.namespace, sourceWindow: input.sourceWindow }); const now = new Date().toISOString(); const row = { id: randomUUID(), user_id: input.userId, request_id: input.requestId, operator_action_id: input.operatorActionId ?? null, namespace: input.namespace, pack_type: "master_candidate", status: c.status, title: c.title, summary: c.summary, source_window: c.source_window, candidate_payload: c.candidate_payload, evidence: c.evidence, warnings: c.warnings, created_at: now, updated_at: now, reviewed_at: null, rejected_at: null, archived_at: null }; const created = await single(client.from("pandora_shadow_context_packs").insert(row).select("*").single()); if (!created) throw new Error("Unable to create shadow context pack candidate"); await createShadowContextPackEvent(client, { userId: input.userId, shadowPackId: created.id, eventType: "shadow_candidate_created", message: "Shadow context-pack candidate created. No production memory mutation or promotion performed.", metadata: { request_id: input.requestId, operator_action_id: input.operatorActionId ?? null, no_core_memory_mutation_performed: true, no_promotion_performed: true } }); return created; }
+export async function updateShadowContextPackStatus(client: ShadowContextPackDbClient, input: { userId: string; shadowPackId: string; status: string; reason?: string }): Promise { assertStatus(input.status); await getShadowContextPack(client, input); const now = new Date().toISOString(); const patch: Record = { status: input.status, updated_at: now }; if (input.status === "reviewed") patch.reviewed_at = now; if (input.status === "rejected") patch.rejected_at = now; if (input.status === "archived") patch.archived_at = now; const updated = await single(client.from("pandora_shadow_context_packs").update(patch).eq("user_id", input.userId).eq("id", input.shadowPackId).select("*").single()); if (!updated) throw new Error("Unable to update shadow context pack for current user"); await createShadowContextPackEvent(client, { userId: input.userId, shadowPackId: input.shadowPackId, eventType: `shadow_candidate_${input.status}`, message: input.reason ? `Shadow candidate marked ${input.status}: ${input.reason}` : `Shadow candidate marked ${input.status}. No promotion performed.`, metadata: { no_core_memory_mutation_performed: true, no_promotion_performed: true } }); return updated; }
diff --git a/supabase/migrations/20260703020000_pandora_shadow_context_pack_lab.sql b/supabase/migrations/20260703020000_pandora_shadow_context_pack_lab.sql
new file mode 100644
index 0000000..c4bbb0b
--- /dev/null
+++ b/supabase/migrations/20260703020000_pandora_shadow_context_pack_lab.sql
@@ -0,0 +1,31 @@
+alter table public.pandora_operator_actions drop constraint if exists pandora_operator_actions_action_type_check;
+alter table public.pandora_operator_actions add constraint pandora_operator_actions_action_type_check check (action_type in ('verify_namespace_invariants','verify_pack_supersession','check_retrieval_eval_status','refresh_dashboard_snapshot','prepare_distill_smoke_plan','prepare_shadow_context_pack'));
+
+create table if not exists public.pandora_shadow_context_packs (
+ id uuid primary key default gen_random_uuid(), user_id uuid not null, request_id text not null,
+ operator_action_id uuid null references public.pandora_operator_actions(id) on delete set null,
+ namespace text not null check (namespace in ('real_life','au')), pack_type text not null default 'master_candidate',
+ status text not null default 'draft' check (status in ('draft','ready_for_review','reviewed','rejected','archived')),
+ title text not null, summary text not null, source_window jsonb not null default '{}'::jsonb,
+ candidate_payload jsonb not null default '{}'::jsonb, evidence jsonb not null default '{}'::jsonb,
+ warnings text[] not null default '{}'::text[], created_at timestamptz not null default now(), updated_at timestamptz not null default now(),
+ reviewed_at timestamptz null, rejected_at timestamptz null, archived_at timestamptz null
+);
+create table if not exists public.pandora_shadow_context_pack_events (
+ id uuid primary key default gen_random_uuid(), shadow_pack_id uuid not null references public.pandora_shadow_context_packs(id) on delete cascade,
+ user_id uuid not null, event_type text not null, message text not null, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now()
+);
+create index if not exists pandora_shadow_context_packs_user_created_idx on public.pandora_shadow_context_packs(user_id, created_at desc);
+create index if not exists pandora_shadow_context_packs_user_namespace_status_idx on public.pandora_shadow_context_packs(user_id, namespace, status);
+create index if not exists pandora_shadow_context_packs_user_action_idx on public.pandora_shadow_context_packs(user_id, operator_action_id);
+create index if not exists pandora_shadow_context_pack_events_user_pack_created_idx on public.pandora_shadow_context_pack_events(user_id, shadow_pack_id, created_at desc);
+alter table public.pandora_shadow_context_packs enable row level security;
+alter table public.pandora_shadow_context_pack_events enable row level security;
+create policy "pandora_shadow_context_packs_select_own" on public.pandora_shadow_context_packs for select to authenticated using (user_id = auth.uid());
+create policy "pandora_shadow_context_packs_insert_own" on public.pandora_shadow_context_packs for insert to authenticated with check (user_id = auth.uid());
+create policy "pandora_shadow_context_packs_update_own" on public.pandora_shadow_context_packs for update to authenticated using (user_id = auth.uid()) with check (user_id = auth.uid());
+create policy "pandora_shadow_context_pack_events_select_own" on public.pandora_shadow_context_pack_events for select to authenticated using (user_id = auth.uid());
+create policy "pandora_shadow_context_pack_events_insert_own" on public.pandora_shadow_context_pack_events for insert to authenticated with check (user_id = auth.uid());
+create or replace function public.set_pandora_shadow_context_pack_updated_at() returns trigger language plpgsql as $$ begin new.updated_at = now(); return new; end; $$;
+drop trigger if exists set_pandora_shadow_context_pack_updated_at on public.pandora_shadow_context_packs;
+create trigger set_pandora_shadow_context_pack_updated_at before update on public.pandora_shadow_context_packs for each row execute function public.set_pandora_shadow_context_pack_updated_at();
diff --git a/tests/unit/pandora-operator-action-guards.test.ts b/tests/unit/pandora-operator-action-guards.test.ts
index 97e1aea..ae05ac8 100644
--- a/tests/unit/pandora-operator-action-guards.test.ts
+++ b/tests/unit/pandora-operator-action-guards.test.ts
@@ -1,10 +1,10 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
-const files = ["components/pandora/OperatorActionCenterCard.tsx", "components/pandora/OperatorActionComposer.tsx", "components/pandora/OperatorActionList.tsx", "components/pandora/OperatorActionEnvelope.tsx", "components/pandora/OperatorActionControls.tsx", "components/pandora/OperatorActionTimeline.tsx"];
+const files = ["components/pandora/OperatorActionCenterCard.tsx", "components/pandora/OperatorActionComposer.tsx", "components/pandora/OperatorActionList.tsx", "components/pandora/OperatorActionEnvelope.tsx", "components/pandora/OperatorActionControls.tsx", "components/pandora/OperatorActionTimeline.tsx", "components/pandora/ShadowContextPackLabCard.tsx", "components/pandora/ShadowContextPackControls.tsx", "components/pandora/ShadowContextPackDetail.tsx"];
describe("operator action center safety guards", () => {
- it("does not render dangerous live action buttons", () => { const text = files.map((f)=>readFileSync(f,"utf8")).join("\n"); for (const bad of ["Run live", "Delete memory", "Prune now", "Merge now", "Distill now"]) expect(text).not.toContain(bad); });
- it("service does not import service-role/admin clients or mutate core memory tables", () => { const text=readFileSync("lib/services/pandora-operator-action-service.ts","utf8"); expect(text).not.toContain("service-role"); expect(text).not.toContain("createSupabaseBridgeAdminClient"); expect(text).not.toContain(".delete("); for (const table of ["memory_events","memory_context_packs","memory_profiles","memory_capture_candidates","memory_pruning_candidates"]) expect(text).not.toMatch(new RegExp(`from\\(\\\"${table}\\\"\\).*\\.(insert|update|delete)`)); expect(text).toContain("no_mutation_performed: true"); });
- it("production pandora path does not import mock-data and route identity rejects client user ids", () => { expect(readFileSync("app/pandora/page.tsx","utf8")).not.toContain("mock-data"); const route=readFileSync("app/api/pandora/operator-actions/route.ts","utf8"); expect(route).toContain("assertNoClientUserIdOverride"); expect(route).not.toContain("searchParams.get(\"user_id\")"); });
+ it("does not render dangerous live action buttons", () => { const text = files.map((f)=>readFileSync(f,"utf8")).join("\n"); for (const bad of ["Promote live", "Replace master", "Delete memory", "Prune now", "Merge now", "Distill now"]) expect(text).not.toContain(bad); });
+ it("service does not import service-role/admin clients or mutate core memory tables", () => { const text=readFileSync("lib/services/pandora-operator-action-service.ts","utf8")+readFileSync("lib/services/pandora-shadow-context-pack-service.ts","utf8"); expect(text).not.toContain("service-role"); expect(text).not.toContain("createSupabaseBridgeAdminClient"); expect(text).not.toContain(".delete("); for (const table of ["memory_events","memory_context_packs","memory_profiles","memory_capture_candidates","memory_pruning_candidates"]) expect(text).not.toMatch(new RegExp(`from\\(\\\"${table}\\\"\\).*\\.(insert|update|delete)`)); expect(text).toContain("no_core_memory_mutation_performed: true"); });
+ it("production pandora path does not import mock-data and route identity rejects client user ids", () => { expect(readFileSync("app/pandora/page.tsx","utf8")).not.toContain("mock-data"); const route=readFileSync("app/api/pandora/operator-actions/route.ts","utf8")+readFileSync("app/api/pandora/shadow-context-packs/route.ts","utf8")+readFileSync("app/api/pandora/shadow-context-packs/[id]/review/route.ts","utf8"); expect(route).toContain("assertNoClientUserIdOverride"); expect(route).not.toContain("searchParams.get(\"user_id\")"); expect(route).not.toContain("body.user_id"); });
it("retrieval eval still has no fabricated accuracy", () => { const text=readFileSync("lib/services/pandora-verification-service.ts","utf8")+readFileSync("lib/services/pandora-dashboard-service.ts","utf8"); expect(text).not.toContain("94.3"); expect(text).not.toContain("fake accuracy"); expect(text).not.toContain("retrieval accuracy: 100%"); });
});
diff --git a/tests/unit/pandora-operator-action-service.test.ts b/tests/unit/pandora-operator-action-service.test.ts
index 9cdc6e3..c66d725 100644
--- a/tests/unit/pandora-operator-action-service.test.ts
+++ b/tests/unit/pandora-operator-action-service.test.ts
@@ -6,7 +6,7 @@ const USER = "11111111-1111-4111-8111-111111111111";
const OTHER = "22222222-2222-4222-8222-222222222222";
type Store = Record;
function client(store: Store) { return { from(table: string) { const eqs: Record = {}; let lim = Infinity; let patch: any; let insertRow: any; const rows=()=> (store[table]??[]).filter(r=>Object.entries(eqs).every(([k,v])=>r[k]===v)).slice(0,lim); const b:any={ select(){return b}, eq(k:string,v:any){eqs[k]=v;return b}, order(){return b}, limit(n:number){lim=n;return b}, single(){return b}, insert(row:any){insertRow=Array.isArray(row)?row[0]:row; store[table]=store[table]??[]; store[table].push(insertRow); return b}, update(row:any){patch=row; return b}, then(res:any,rej:any){ if(patch){ const r=rows()[0]; if(r) Object.assign(r,patch); return Promise.resolve({data:r??null,error:r?null:{message:"missing"}}).then(res,rej)} return Promise.resolve({data:insertRow??rows(),error:null}).then(res,rej)}}; return b; }} as any; }
-function store(): Store { return { pandora_operator_actions: [], pandora_operator_action_events: [], memory_context_packs: [{ id:"rl", user_id: USER, namespace:"real_life", pack_type:"master", status:"active", title:"RL", created_at:"2026-07-03" }, { id:"au", user_id: USER, namespace:"au", pack_type:"master", status:"active", title:"AU", created_at:"2026-07-03" }], audit_logs: [], retrieval_logs: [], memory_events: [], memory_profiles: [], memory_open_loops: [], memory_capture_candidates: [], memory_review_queue_items: [], memory_pruning_candidates: [] }; }
+function store(): Store { return { pandora_operator_actions: [], pandora_operator_action_events: [], pandora_shadow_context_packs: [], pandora_shadow_context_pack_events: [], memory_context_packs: [{ id:"rl", user_id: USER, namespace:"real_life", pack_type:"master", status:"active", title:"RL", created_at:"2026-07-03" }, { id:"au", user_id: USER, namespace:"au", pack_type:"master", status:"active", title:"AU", created_at:"2026-07-03" }], audit_logs: [], retrieval_logs: [], memory_events: [{ id:"e1", user_id: USER, namespace:"real_life", status:"reviewed", extracted_summary:"RL event", created_at:"2026-07-03" }], memory_profiles: [], memory_open_loops: [], memory_capture_candidates: [], memory_review_queue_items: [], memory_pruning_candidates: [] }; }
describe("pandora operator action service", () => {
it("proposes allowed action and returns existing duplicate by idempotency", async () => { const s=store(); const c=client(s); const a=await proposeOperatorAction(c,{userId:USER, actionType:"verify_namespace_invariants", namespace:"real_life", mode:"dry_run", payload:{a:1}}); const b=await proposeOperatorAction(c,{userId:USER, actionType:"verify_namespace_invariants", namespace:"real_life", mode:"dry_run", payload:{a:1}}); expect(a.id).toBe(b.id); expect(s.pandora_operator_actions).toHaveLength(1); });
@@ -17,5 +17,6 @@ describe("pandora operator action service", () => {
it("cancel only own action", async () => { const s=store(); const c=client(s); const a=await proposeOperatorAction(c,{userId:USER, actionType:"check_retrieval_eval_status"}); await expect(cancelOperatorAction(c,{userId:OTHER, actionId:a.id})).rejects.toThrow(/not found/); const cancelled=await cancelOperatorAction(c,{userId:USER, actionId:a.id}); expect(cancelled.status).toBe("cancelled"); });
it("approves proposed and dry_ran actions, then executes approved read-only action", async () => { const s=store(); const c=client(s); const a=await proposeOperatorAction(c,{userId:USER, actionType:"verify_pack_supersession", namespace:"real_life"}); const approved=await approveOperatorAction(c,{userId:USER, actionId:a.id}); expect(approved.status).toBe("approved"); const completed=await executeApprovedOperatorAction(c,{userId:USER, actionId:a.id}); expect(completed.status).toBe("completed"); expect(completed.result.no_mutation_performed).toBe(true); expect(JSON.stringify(completed.result)).toContain("pack_supersession"); expect(s.memory_context_packs).toHaveLength(2); });
it("blocks invalid transitions and cross-user events", async () => { const s=store(); const c=client(s); const a=await proposeOperatorAction(c,{userId:USER, actionType:"refresh_dashboard_snapshot"}); await expect(executeApprovedOperatorAction(c,{userId:USER, actionId:a.id})).rejects.toThrow(/Invalid/); await expect(approveOperatorAction(c,{userId:OTHER, actionId:a.id})).rejects.toThrow(/not found/); await expect(listOperatorActionEvents(c,{userId:OTHER, actionId:a.id})).rejects.toThrow(/not found/); const cancelled=await cancelOperatorAction(c,{userId:USER, actionId:a.id}); await expect(cancelOperatorAction(c,{userId:USER, actionId:cancelled.id})).rejects.toThrow(/terminal/); });
+ it("prepare_shadow_context_pack dry-run and approved execute stage only shadow rows", async () => { const s=store(); const c=client(s); const a=await proposeOperatorAction(c,{userId:USER, actionType:"prepare_shadow_context_pack", namespace:"real_life", payload:{source:"operator_action_center"}}); const d=await dryRunOperatorAction(c,{userId:USER, actionId:a.id}); expect(d.result.evidence_summary).toMatchObject({ shadow_write_performed:false, no_core_memory_mutation_performed:true, no_promotion_performed:true }); expect(s.pandora_shadow_context_packs).toHaveLength(0); await approveOperatorAction(c,{userId:USER, actionId:a.id}); const completed=await executeApprovedOperatorAction(c,{userId:USER, actionId:a.id}); expect(completed.status).toBe("completed"); expect(completed.result.evidence_summary).toMatchObject({ shadow_write_performed:true, no_core_memory_mutation_performed:true, no_promotion_performed:true }); expect(s.pandora_shadow_context_packs).toHaveLength(1); expect(s.pandora_shadow_context_pack_events).toHaveLength(1); expect(s.memory_context_packs).toHaveLength(2); });
it("check_retrieval_eval_status reports unavailable evidence without fabricated accuracy", async () => { const s=store(); const c=client(s); const a=await proposeOperatorAction(c,{userId:USER, actionType:"check_retrieval_eval_status"}); await approveOperatorAction(c,{userId:USER, actionId:a.id}); const completed=await executeApprovedOperatorAction(c,{userId:USER, actionId:a.id}); expect(JSON.stringify(completed.result)).toContain("retrieval_eval_status"); expect(JSON.stringify(completed.result)).not.toContain("94.3"); });
});
diff --git a/tests/unit/pandora-shadow-context-pack-service.test.ts b/tests/unit/pandora-shadow-context-pack-service.test.ts
new file mode 100644
index 0000000..9f5bafd
--- /dev/null
+++ b/tests/unit/pandora-shadow-context-pack-service.test.ts
@@ -0,0 +1,16 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { describe, expect, it } from "vitest";
+import { createShadowContextPackCandidate, dryRunShadowContextPackCandidate, listShadowContextPacks, updateShadowContextPackStatus } from "@/lib/services/pandora-shadow-context-pack-service";
+const USER="11111111-1111-4111-8111-111111111111", OTHER="22222222-2222-4222-8222-222222222222";
+type Store=Record;
+function client(store:Store){ return { from(table:string){ const eqs:Record={}; let lim=Infinity; let patch:any; let inserted:any; const rows=()=> (store[table]??[]).filter(r=>Object.entries(eqs).every(([k,v])=>r[k]===v)).slice(0,lim); const b:any={ select(){return b}, eq(k:string,v:any){eqs[k]=v;return b}, order(){return b}, limit(n:number){lim=n;return b}, single(){return b}, insert(row:any){inserted=Array.isArray(row)?row[0]:row; store[table]=store[table]??[]; store[table].push(inserted); return b}, update(row:any){patch=row; return b}, then(res:any,rej:any){ if(patch){ const r=rows()[0]; if(r) Object.assign(r,patch); return Promise.resolve({data:r??null,error:r?null:{message:"missing"}}).then(res,rej)} return Promise.resolve({data:inserted??rows(),error:null}).then(res,rej)}}; return b; }} as any; }
+function store():Store{return { memory_events:[{id:"e1",user_id:USER,namespace:"real_life",status:"reviewed",extracted_summary:"Real project update",created_at:"2026-07-03"},{id:"e2",user_id:USER,namespace:"au",status:"promoted",extracted_summary:"AU story beat",created_at:"2026-07-02"},{id:"e3",user_id:OTHER,namespace:"real_life",status:"reviewed",extracted_summary:"Other user",created_at:"2026-07-03"}], memory_context_packs:[{id:"m1",user_id:USER,namespace:"real_life",pack_type:"master",status:"active",created_at:"2026-07-01"},{id:"m2",user_id:USER,namespace:"au",pack_type:"master",status:"active",created_at:"2026-07-01"}], memory_open_loops:[{id:"l1",user_id:USER,namespace:"real_life",status:"open",created_at:"2026-07-03"}], memory_review_queue_items:[{id:"r1",user_id:USER,namespace:"real_life",status:"pending_review",created_at:"2026-07-03"}], pandora_shadow_context_packs:[], pandora_shadow_context_pack_events:[]};}
+describe("shadow context pack service",()=>{
+ it("dry-runs deterministically for real_life and never includes au rows", async()=>{const s=store(); const r=await dryRunShadowContextPackCandidate(client(s),{userId:USER,namespace:"real_life"}); expect(r.summary).toBe("Shadow candidate for real_life: 1 events considered, 1 open loops, 1 review items. Promotion is gated."); expect(JSON.stringify(r)).toContain("Real project update"); expect(JSON.stringify(r)).not.toContain("AU story beat");});
+ it("dry-runs deterministically for au", async()=>{const r=await dryRunShadowContextPackCandidate(client(store()),{userId:USER,namespace:"au"}); expect(r.summary).toContain("Shadow candidate for au: 1 events considered"); expect(r.status).toBe("ready_for_review");});
+ it("another user's rows never appear", async()=>{const r=await dryRunShadowContextPackCandidate(client(store()),{userId:OTHER,namespace:"real_life"}); expect(JSON.stringify(r)).toContain("Other user"); expect(JSON.stringify(r)).not.toContain("Real project update");});
+ it("creates only shadow row and matching event", async()=>{const s=store(); const pack=await createShadowContextPackCandidate(client(s),{userId:USER,namespace:"real_life",requestId:"req"}); expect(pack.id).toBeTruthy(); expect(s.pandora_shadow_context_packs).toHaveLength(1); expect(s.pandora_shadow_context_pack_events).toHaveLength(1); expect(s.memory_events).toHaveLength(3); expect(s.memory_context_packs).toHaveLength(2);});
+ it("missing evidence warns and stays draft", async()=>{const s=store(); s.memory_events=[]; s.memory_context_packs=[]; const r=await dryRunShadowContextPackCandidate(client(s),{userId:USER,namespace:"real_life"}); expect(r.status).toBe("draft"); expect(r.warnings.join(" ")).toContain("No active master"); expect(r.warnings.join(" ")).toContain("No memory_events");});
+ it("updates status only for own pack and supports terminal review states", async()=>{const s=store(); const c=client(s); const p=await createShadowContextPackCandidate(c,{userId:USER,namespace:"real_life",requestId:"req"}); expect((await updateShadowContextPackStatus(c,{userId:USER,shadowPackId:p.id,status:"reviewed"})).status).toBe("reviewed"); expect((await updateShadowContextPackStatus(c,{userId:USER,shadowPackId:p.id,status:"rejected"})).status).toBe("rejected"); expect((await updateShadowContextPackStatus(c,{userId:USER,shadowPackId:p.id,status:"archived"})).status).toBe("archived"); await expect(updateShadowContextPackStatus(c,{userId:OTHER,shadowPackId:p.id,status:"reviewed"})).rejects.toThrow(/not found/);});
+ it("list filters own namespace/status", async()=>{const s=store(); const c=client(s); await createShadowContextPackCandidate(c,{userId:USER,namespace:"real_life",requestId:"req"}); await createShadowContextPackCandidate(c,{userId:USER,namespace:"au",requestId:"req2"}); expect(await listShadowContextPacks(c,{userId:USER,namespace:"real_life"})).toHaveLength(1);});
+});