@vantageos/backend-doctor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +94 -0
- package/dist/contract.d.ts +18 -0
- package/dist/contract.js +29 -0
- package/dist/coverage-cli.d.ts +2 -0
- package/dist/coverage-cli.js +17 -0
- package/dist/coverage.d.ts +32 -0
- package/dist/coverage.js +77 -0
- package/dist/detectors/oracle-axis1.d.ts +86 -0
- package/dist/detectors/oracle-axis1.js +221 -0
- package/dist/detectors/oracle-axis2.d.ts +22 -0
- package/dist/detectors/oracle-axis2.js +149 -0
- package/dist/detectors/predicates.d.ts +19 -0
- package/dist/detectors/predicates.js +334 -0
- package/dist/detectors/shared.d.ts +56 -0
- package/dist/detectors/shared.js +103 -0
- package/dist/detectors/source/_scan.d.ts +43 -0
- package/dist/detectors/source/_scan.js +82 -0
- package/dist/detectors/source/backfill.d.ts +27 -0
- package/dist/detectors/source/backfill.js +211 -0
- package/dist/detectors/source/contract.d.ts +33 -0
- package/dist/detectors/source/contract.js +385 -0
- package/dist/detectors/source/identity.d.ts +47 -0
- package/dist/detectors/source/identity.js +410 -0
- package/dist/detectors/source/jobs.d.ts +10 -0
- package/dist/detectors/source/jobs.js +209 -0
- package/dist/detectors/source/lifecycle.d.ts +6 -0
- package/dist/detectors/source/lifecycle.js +147 -0
- package/dist/detectors/source/readbudget.d.ts +6 -0
- package/dist/detectors/source/readbudget.js +133 -0
- package/dist/detectors/source/refusal.d.ts +6 -0
- package/dist/detectors/source/refusal.js +125 -0
- package/dist/detectors/source/security.d.ts +37 -0
- package/dist/detectors/source/security.js +392 -0
- package/dist/detectors/source/writesafety.d.ts +14 -0
- package/dist/detectors/source/writesafety.js +345 -0
- package/dist/detectors/standard/_doc.d.ts +66 -0
- package/dist/detectors/standard/_doc.js +152 -0
- package/dist/detectors/standard/r41-frontier.d.ts +58 -0
- package/dist/detectors/standard/r41-frontier.js +176 -0
- package/dist/detectors/standard/r46-returns-validator.d.ts +61 -0
- package/dist/detectors/standard/r46-returns-validator.js +474 -0
- package/dist/detectors/standard/standard-doc.d.ts +12 -0
- package/dist/detectors/standard/standard-doc.js +271 -0
- package/dist/detectors/structural.d.ts +14 -0
- package/dist/detectors/structural.js +83 -0
- package/dist/engine-types.d.ts +10 -0
- package/dist/engine-types.js +1 -0
- package/dist/not-checkable.d.ts +22 -0
- package/dist/not-checkable.js +37 -0
- package/dist/oracle.d.ts +33 -0
- package/dist/oracle.js +133 -0
- package/dist/registry.d.ts +11 -0
- package/dist/registry.js +140 -0
- package/dist/report.d.ts +35 -0
- package/dist/report.js +138 -0
- package/dist/severity.d.ts +30 -0
- package/dist/severity.js +121 -0
- package/dist/tally.d.ts +57 -0
- package/dist/tally.js +152 -0
- package/dist/trigger.d.ts +47 -0
- package/dist/trigger.js +143 -0
- package/fixtures/backend-standard-51ec7af1.md +648 -0
- package/fixtures/backend-standard-a9d7877b.md +629 -0
- package/fixtures/mcp-app-standard-a74a042b.md +279 -0
- package/fixtures/mcp-extension-standard-a74a042b.md +259 -0
- package/fixtures/mcp-server-standard-a74a042b.md +434 -0
- package/fixtures/mcp-standard-a74a042b.md +354 -0
- package/fixtures/vp-1200-green/.pin +1 -0
- package/fixtures/vp-1200-green/convex/migrations.ts +350 -0
- package/fixtures/vp-1200-green/convex/schema.ts +1243 -0
- package/fixtures/vp-1200-red/.pin +1 -0
- package/fixtures/vp-1200-red/convex/migrations.ts +239 -0
- package/fixtures/vp-1200-red/convex/schema.ts +1243 -0
- package/fixtures/vp-by-tool-day158.csv +190 -0
- package/fixtures/vp-convex/.github/workflows/npm-publish.yml +163 -0
- package/fixtures/vp-convex/.pin +1 -0
- package/fixtures/vp-convex/bun.lock +783 -0
- package/fixtures/vp-convex/convex/auth.config.ts +8 -0
- package/fixtures/vp-convex/convex/briefingNotes.ts +321 -0
- package/fixtures/vp-convex/convex/convex.config.ts +14 -0
- package/fixtures/vp-convex/convex/credentials.ts +591 -0
- package/fixtures/vp-convex/convex/crons.ts +72 -0
- package/fixtures/vp-convex/convex/dashboard.ts +199 -0
- package/fixtures/vp-convex/convex/gumroadWebhook.ts +815 -0
- package/fixtures/vp-convex/convex/issues.ts +544 -0
- package/fixtures/vp-convex/convex/kb.ts +305 -0
- package/fixtures/vp-convex/convex/kbMutations.ts +192 -0
- package/fixtures/vp-convex/convex/kbShared.ts +56 -0
- package/fixtures/vp-convex/convex/licenses.ts +443 -0
- package/fixtures/vp-convex/convex/memories.ts +406 -0
- package/fixtures/vp-convex/convex/memoriesScoped.ts +220 -0
- package/fixtures/vp-convex/convex/messages.ts +971 -0
- package/fixtures/vp-convex/convex/migrations.ts +239 -0
- package/fixtures/vp-convex/convex/missionTemplates.ts +369 -0
- package/fixtures/vp-convex/convex/oauth.ts +1608 -0
- package/fixtures/vp-convex/convex/oauthMigrations.ts +96 -0
- package/fixtures/vp-convex/convex/okfBundleNode.ts +1031 -0
- package/fixtures/vp-convex/convex/recurringTasks.ts +386 -0
- package/fixtures/vp-convex/convex/schema.ts +1229 -0
- package/fixtures/vp-convex/convex/tasks.ts +2782 -0
- package/fixtures/vp-r31-green/.pin +1 -0
- package/fixtures/vp-r31-green/convex/migrations.ts +350 -0
- package/fixtures/vp-r31-red/.pin +1 -0
- package/fixtures/vp-r31-red/convex/migrations.ts +286 -0
- package/fixtures/vr-by-tool-day159.csv +89 -0
- package/package.json +39 -0
|
@@ -0,0 +1,1243 @@
|
|
|
1
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// Shared validators (reused across tables)
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export const memoryTypeValidator = v.union(
|
|
9
|
+
v.literal("user"),
|
|
10
|
+
v.literal("feedback"),
|
|
11
|
+
v.literal("project"),
|
|
12
|
+
v.literal("reference"),
|
|
13
|
+
v.literal("episode"),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
// Open validator — any orchestrator name is accepted.
|
|
17
|
+
// Known defaults: pi, tau, phi, sigma, omega, zeta, eta, kappa, alpha, lambda,
|
|
18
|
+
// victor, proxima, theta, xi, epsilon, omicron, upsilon, system.
|
|
19
|
+
// New orchestrators can be added without schema changes (see issue #132).
|
|
20
|
+
export const creatorValidator = v.string();
|
|
21
|
+
|
|
22
|
+
// Day 130 follow-up #2 (Eta REVISE, PR #1089) — inforgeable automation
|
|
23
|
+
// signal. `createdBy` is a caller-supplied string on the PUBLIC
|
|
24
|
+
// `tasks.create` mutation, so it can be forged (e.g. `createdBy: "system"`)
|
|
25
|
+
// to escape the billing closure gate. `origin` is NOT accepted as an
|
|
26
|
+
// argument on any public mutation — only the internal webhook path
|
|
27
|
+
// (createOrUpdateReviewTask) writes it. The closure gate reads `origin`,
|
|
28
|
+
// never `createdBy`, to decide whether a task is automation-created.
|
|
29
|
+
export const taskOriginValidator = v.literal("automation");
|
|
30
|
+
|
|
31
|
+
export const relationTypeValidator = v.union(
|
|
32
|
+
v.literal("updates"),
|
|
33
|
+
v.literal("extends"),
|
|
34
|
+
v.literal("derives"),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export const severityValidator = v.union(
|
|
38
|
+
v.literal("critical"),
|
|
39
|
+
v.literal("major"),
|
|
40
|
+
v.literal("minor"),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
// Schema
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export default defineSchema({
|
|
48
|
+
// ── memories ────────────────────────────────────────────────────────────────
|
|
49
|
+
// Core memory store. Each row is one typed memory entry.
|
|
50
|
+
// Supermemory pattern: updates/extends/derives relations + isLatest flag.
|
|
51
|
+
// Mem0 pattern: typed memory (user/feedback/project/reference/episode).
|
|
52
|
+
// Episodic type includes structured episode metadata.
|
|
53
|
+
//
|
|
54
|
+
// Embedding + search is handled by @convex-dev/rag (component-owned schema).
|
|
55
|
+
// RAG entry key = memoryId string. Filters: namespace, type, isLatest.
|
|
56
|
+
memories: defineTable({
|
|
57
|
+
// Namespace: "global" | "orchestrator/pi" | "project/vantage-starter"
|
|
58
|
+
namespace: v.string(),
|
|
59
|
+
|
|
60
|
+
// Memory classification — drives retrieval strategy
|
|
61
|
+
type: memoryTypeValidator,
|
|
62
|
+
|
|
63
|
+
// Human-readable content (what the memory says)
|
|
64
|
+
content: v.string(),
|
|
65
|
+
|
|
66
|
+
// Which orchestrator (or system) created this memory
|
|
67
|
+
createdBy: creatorValidator,
|
|
68
|
+
instanceId: v.optional(v.string()), // which instance wrote this
|
|
69
|
+
|
|
70
|
+
// Graph relations to other memories (Supermemory pattern)
|
|
71
|
+
// "updates" supersedes target (sets target.isLatest = false)
|
|
72
|
+
// "extends" adds detail to target (both remain latest)
|
|
73
|
+
// "derives" is an inference drawn from target
|
|
74
|
+
relations: v.array(
|
|
75
|
+
v.object({
|
|
76
|
+
targetId: v.id("memories"),
|
|
77
|
+
type: relationTypeValidator,
|
|
78
|
+
}),
|
|
79
|
+
),
|
|
80
|
+
|
|
81
|
+
// True = this is the current authoritative version.
|
|
82
|
+
// False = superseded by a newer "updates" relation.
|
|
83
|
+
// Search always filters isLatest=true by default.
|
|
84
|
+
isLatest: v.boolean(),
|
|
85
|
+
|
|
86
|
+
// Optional TTL hint (ISO string). Cron job handles actual expiry.
|
|
87
|
+
// Example: "2026-06-01T00:00:00Z"
|
|
88
|
+
ttl: v.optional(v.string()),
|
|
89
|
+
|
|
90
|
+
// Episodic memory payload — only present when type="episode"
|
|
91
|
+
episode: v.optional(
|
|
92
|
+
v.object({
|
|
93
|
+
context: v.string(), // Situation that triggered this episode
|
|
94
|
+
goal: v.string(), // What was being attempted
|
|
95
|
+
action: v.string(), // What was actually done
|
|
96
|
+
outcome: v.string(), // What happened
|
|
97
|
+
insight: v.string(), // The lesson extracted (procedural memory)
|
|
98
|
+
severity: severityValidator,
|
|
99
|
+
}),
|
|
100
|
+
),
|
|
101
|
+
|
|
102
|
+
// Timestamp (ms since epoch)
|
|
103
|
+
createdAt: v.number(),
|
|
104
|
+
updatedAt: v.number(),
|
|
105
|
+
})
|
|
106
|
+
// Primary query patterns — all filtered to isLatest for active memory reads
|
|
107
|
+
.index("by_namespace", ["namespace", "isLatest"])
|
|
108
|
+
.index("by_type", ["type", "isLatest"])
|
|
109
|
+
.index("by_creator", ["createdBy", "isLatest"])
|
|
110
|
+
.index("by_namespace_type", ["namespace", "type", "isLatest"]),
|
|
111
|
+
|
|
112
|
+
// ── profiles ────────────────────────────────────────────────────────────────
|
|
113
|
+
// One row per INSTANCE. orchestratorId = role (pi/tau/phi).
|
|
114
|
+
// instanceId = unique running copy (pi-chromebook, pi-vps, tau-client-acme).
|
|
115
|
+
// Multiple instances can share the same role.
|
|
116
|
+
// static = stable identity facts (role, workspace, capabilities)
|
|
117
|
+
// dynamic = mutable session state (current task, last seen, session count)
|
|
118
|
+
profiles: defineTable({
|
|
119
|
+
// Role: "pi" | "tau" | "phi"
|
|
120
|
+
orchestratorId: v.string(),
|
|
121
|
+
|
|
122
|
+
// Unique instance identifier: "pi-chromebook" | "pi-vps" | "tau-vps-1"
|
|
123
|
+
instanceId: v.optional(v.string()),
|
|
124
|
+
|
|
125
|
+
name: v.string(),
|
|
126
|
+
|
|
127
|
+
// Static profile — stable facts, infrequently updated
|
|
128
|
+
static: v.object({
|
|
129
|
+
role: v.string(),
|
|
130
|
+
workspace: v.string(),
|
|
131
|
+
capabilities: v.array(v.string()),
|
|
132
|
+
}),
|
|
133
|
+
|
|
134
|
+
// Dynamic profile — updated each session
|
|
135
|
+
dynamic: v.object({
|
|
136
|
+
currentTask: v.optional(v.string()),
|
|
137
|
+
// Durable end-of-day index (close-day step 9) — distinct from
|
|
138
|
+
// `currentTask` (live session summary) so a next-morning
|
|
139
|
+
// daily-start write does not clobber it. mission k574p02m DEFECT 1.
|
|
140
|
+
endOfDayIndex: v.optional(v.string()),
|
|
141
|
+
lastSeen: v.number(), // ms since epoch
|
|
142
|
+
sessionCount: v.number(),
|
|
143
|
+
}),
|
|
144
|
+
})
|
|
145
|
+
.index("by_orchestrator", ["orchestratorId"])
|
|
146
|
+
.index("by_instance", ["instanceId"]),
|
|
147
|
+
|
|
148
|
+
// ── messages ──────────────────────────────────────────────────────────────
|
|
149
|
+
// Inter-orchestrator messaging. VantagePeers native (supersedes pre-VP legacy).
|
|
150
|
+
// One message row per send. Recipients tracked via messageReceipts table.
|
|
151
|
+
// channel: "broadcast" | "pi" | "tau" | "phi" | "pi,tau"
|
|
152
|
+
messages: defineTable({
|
|
153
|
+
from: creatorValidator,
|
|
154
|
+
fromInstanceId: v.optional(v.string()), // "pi-chromebook", "tau-vps-1"
|
|
155
|
+
tenantId: v.optional(v.string()),
|
|
156
|
+
channel: v.string(),
|
|
157
|
+
content: v.string(),
|
|
158
|
+
sessionDay: v.optional(v.number()),
|
|
159
|
+
createdAt: v.number(),
|
|
160
|
+
})
|
|
161
|
+
.index("by_day", ["sessionDay"])
|
|
162
|
+
.index("by_from", ["from", "createdAt"])
|
|
163
|
+
.index("by_channel", ["channel", "createdAt"])
|
|
164
|
+
.index("by_tenant_channel", ["tenantId", "channel", "createdAt"])
|
|
165
|
+
// Day 103 — tenant-scoped listing for Clerk callers (task k176wgsrhha0fr0dxxahctvhw588q5a1).
|
|
166
|
+
// by_tenant_created lets listMessages push tenantId BEFORE .take(limit) for
|
|
167
|
+
// non-master callers, preventing under-fill when fleet (null-tenant) traffic
|
|
168
|
+
// dominates the recent window. by_tenant_channel cannot be reused here because
|
|
169
|
+
// channel is a required equality field in that compound (can't skip to createdAt).
|
|
170
|
+
.index("by_tenant_created", ["tenantId", "createdAt"])
|
|
171
|
+
// Day 102 v2.11.0 — CRUD baseline PR-C-bis option B (mission k575kc1r):
|
|
172
|
+
// Convex native BM25 search on message body, with filterFields for the
|
|
173
|
+
// common audit narrowing axes (from, channel, sessionDay).
|
|
174
|
+
.searchIndex("search_content", {
|
|
175
|
+
searchField: "content",
|
|
176
|
+
filterFields: ["from", "channel", "sessionDay", "tenantId"],
|
|
177
|
+
}),
|
|
178
|
+
|
|
179
|
+
// ── messageReceipts ──────────────────────────────────────────────────────
|
|
180
|
+
// One row per recipient per message. Tracks read status.
|
|
181
|
+
// check_new_messages = query receipts where recipient=X AND readAt=undefined
|
|
182
|
+
messageReceipts: defineTable({
|
|
183
|
+
messageId: v.id("messages"),
|
|
184
|
+
recipient: creatorValidator, // role-level: "pi" | "tau" | "phi"
|
|
185
|
+
recipientInstanceId: v.optional(v.string()), // instance-level: "pi-vps"
|
|
186
|
+
tenantId: v.optional(v.string()),
|
|
187
|
+
readAt: v.optional(v.number()), // undefined = unread, ms epoch = read
|
|
188
|
+
})
|
|
189
|
+
.index("by_recipient_unread", ["recipient", "readAt"])
|
|
190
|
+
.index("by_instance_unread", ["recipientInstanceId", "readAt"])
|
|
191
|
+
.index("by_message", ["messageId"])
|
|
192
|
+
.index("by_tenant_recipient_unread", ["tenantId", "recipient", "readAt"]),
|
|
193
|
+
|
|
194
|
+
// ── missions ──────────────────────────────────────────────────────────────
|
|
195
|
+
missions: defineTable({
|
|
196
|
+
name: v.string(),
|
|
197
|
+
description: v.optional(v.string()),
|
|
198
|
+
project: v.string(),
|
|
199
|
+
status: v.union(
|
|
200
|
+
v.literal("brainstorm"),
|
|
201
|
+
v.literal("plan"),
|
|
202
|
+
v.literal("execute"),
|
|
203
|
+
v.literal("validate"),
|
|
204
|
+
v.literal("complete"),
|
|
205
|
+
v.literal("cancelled"),
|
|
206
|
+
),
|
|
207
|
+
priority: v.union(
|
|
208
|
+
v.literal("urgent"),
|
|
209
|
+
v.literal("high"),
|
|
210
|
+
v.literal("medium"),
|
|
211
|
+
v.literal("low"),
|
|
212
|
+
),
|
|
213
|
+
pilot: creatorValidator,
|
|
214
|
+
agents: v.array(v.string()),
|
|
215
|
+
brief: v.optional(v.string()),
|
|
216
|
+
startDate: v.optional(v.number()),
|
|
217
|
+
targetDate: v.optional(v.number()),
|
|
218
|
+
progress: v.optional(v.number()),
|
|
219
|
+
createdBy: creatorValidator,
|
|
220
|
+
createdAt: v.number(),
|
|
221
|
+
updatedAt: v.number(),
|
|
222
|
+
// Beta multi-tenant scope. null/undefined = master (internal Alpha).
|
|
223
|
+
// Set to Clerk org slug (e.g. "acme-hr") for client-scoped rows.
|
|
224
|
+
orgId: v.optional(v.string()),
|
|
225
|
+
// Terminal cancelled status (Day 157) — creator-only, mandatory reason.
|
|
226
|
+
// Never counted as done/complete; excluded from open/active aliases.
|
|
227
|
+
cancelledBy: v.optional(creatorValidator),
|
|
228
|
+
cancelReason: v.optional(v.string()),
|
|
229
|
+
})
|
|
230
|
+
.index("by_project", ["project", "status"])
|
|
231
|
+
.index("by_pilot", ["pilot", "status"])
|
|
232
|
+
.index("by_status", ["status", "createdAt"])
|
|
233
|
+
.index("by_priority", ["priority", "status"])
|
|
234
|
+
.index("by_orgId", ["orgId"]),
|
|
235
|
+
|
|
236
|
+
// ── tasks ──────────────────────────────────────────────────────────────────
|
|
237
|
+
tasks: defineTable({
|
|
238
|
+
title: v.string(),
|
|
239
|
+
description: v.optional(v.string()),
|
|
240
|
+
project: v.optional(v.string()), // "vantage-starter", "perfect-ai-agent", etc.
|
|
241
|
+
tags: v.optional(v.array(v.string())),
|
|
242
|
+
// Orchestrator or person assigned — open string (issue #132)
|
|
243
|
+
assignedTo: v.string(),
|
|
244
|
+
priority: v.union(
|
|
245
|
+
v.literal("urgent"),
|
|
246
|
+
v.literal("high"),
|
|
247
|
+
v.literal("medium"),
|
|
248
|
+
v.literal("low"),
|
|
249
|
+
),
|
|
250
|
+
status: v.union(
|
|
251
|
+
v.literal("todo"),
|
|
252
|
+
v.literal("in_progress"),
|
|
253
|
+
v.literal("review"),
|
|
254
|
+
v.literal("blocked"),
|
|
255
|
+
v.literal("done"),
|
|
256
|
+
v.literal("cancelled"),
|
|
257
|
+
),
|
|
258
|
+
completionNote: v.optional(v.string()), // what was actually done — written on complete/review
|
|
259
|
+
assignedToInstance: v.optional(v.string()), // instance-level assignment: "pi-vps", "tau-chromebook"
|
|
260
|
+
claimedByInstance: v.optional(v.string()), // which instance is working on this
|
|
261
|
+
dependsOn: v.optional(v.array(v.id("tasks"))), // tasks that must be done first
|
|
262
|
+
missionId: v.optional(v.id("missions")),
|
|
263
|
+
estimatedMinutes: v.optional(v.number()),
|
|
264
|
+
actualMinutes: v.optional(v.number()),
|
|
265
|
+
startedAt: v.optional(v.number()),
|
|
266
|
+
completedAt: v.optional(v.number()),
|
|
267
|
+
dueDate: v.optional(v.number()),
|
|
268
|
+
createdBy: creatorValidator,
|
|
269
|
+
createdAt: v.number(),
|
|
270
|
+
updatedAt: v.number(),
|
|
271
|
+
// Beta multi-tenant scope. null/undefined = master (internal Alpha).
|
|
272
|
+
// Set to Clerk org slug (e.g. "acme-hr") for client-scoped rows.
|
|
273
|
+
orgId: v.optional(v.string()),
|
|
274
|
+
// Day 130 follow-up #2 — inforgeable automation signal. ONLY the
|
|
275
|
+
// internal webhook mutation (createOrUpdateReviewTask) writes this;
|
|
276
|
+
// the public `tasks.create` mutation does not accept it as an arg.
|
|
277
|
+
// The billing closure gate reads this field, never `createdBy`
|
|
278
|
+
// (which is a caller-supplied, forgeable string on a public mutation).
|
|
279
|
+
origin: v.optional(taskOriginValidator),
|
|
280
|
+
// Terminal cancelled status (Day 157) — creator-only, mandatory reason.
|
|
281
|
+
// Never counted as done/complete; excluded from open/active aliases.
|
|
282
|
+
cancelledBy: v.optional(creatorValidator),
|
|
283
|
+
cancelReason: v.optional(v.string()),
|
|
284
|
+
// Day 159 — a block that names a commitment, not just a journal entry.
|
|
285
|
+
// blockedOnTaskId: the live task (assigned to someone else) this task
|
|
286
|
+
// waits on. blockTask validates existence/openness/other-assignee at
|
|
287
|
+
// call time; when the cited task reaches "done" every waiter here is
|
|
288
|
+
// swept back to "todo" (convex/tasks.ts unblockWaitersOn). Mutually
|
|
289
|
+
// exclusive with blockedOnNobodyReason in practice (blockTask sets
|
|
290
|
+
// exactly one), both kept optional so pre-existing "blocked" rows with
|
|
291
|
+
// neither field are valid (see migration list query
|
|
292
|
+
// convex/tasks.ts:listUnlinkedBlocked).
|
|
293
|
+
blockedOnTaskId: v.optional(v.id("tasks")),
|
|
294
|
+
// Explicit "# blocked-on-nobody: <reason>" marker text — required to
|
|
295
|
+
// block WITHOUT a blockedOnTaskId (nobody in the fleet owns the
|
|
296
|
+
// obstacle: operator decision, third-party outage). Forbids ANONYMOUS
|
|
297
|
+
// blocking without forbidding blocking itself.
|
|
298
|
+
blockedOnNobodyReason: v.optional(v.string()),
|
|
299
|
+
})
|
|
300
|
+
.index("by_assignee", ["assignedTo", "status"])
|
|
301
|
+
.index("by_project", ["project", "status"])
|
|
302
|
+
.index("by_priority", ["priority", "status"])
|
|
303
|
+
.index("by_status", ["status", "createdAt"])
|
|
304
|
+
.index("by_mission", ["missionId", "status"])
|
|
305
|
+
.index("by_instance", ["assignedToInstance", "status"])
|
|
306
|
+
.index("by_orgId", ["orgId"])
|
|
307
|
+
// Compound indexes added to close the silent-filter-drop defect in
|
|
308
|
+
// convex/tasks.ts `list`: when a caller supplies assignedTo/assignedToInstance
|
|
309
|
+
// TOGETHER with project, the query must apply BOTH filters via a matching
|
|
310
|
+
// index, never pick one and silently discard the other.
|
|
311
|
+
.index("by_assignee_project", ["assignedTo", "project", "status"])
|
|
312
|
+
.index("by_instance_project", ["assignedToInstance", "project", "status"])
|
|
313
|
+
// billingSummaryByProject (convex/tasks.ts) — the period MUST bound the
|
|
314
|
+
// query itself, not a post-hoc in-memory filter over a fixed-size scan
|
|
315
|
+
// (Day-131 live-defect fix: recent, billable work was silently invisible
|
|
316
|
+
// because the old handler capped the by_status(status, createdAt) scan
|
|
317
|
+
// BEFORE ever comparing completedAt against the requested window).
|
|
318
|
+
.index("by_status_completedAt", ["status", "completedAt"])
|
|
319
|
+
// Same fix, pushed one level further: when the caller also supplies a
|
|
320
|
+
// project filter, apply BOTH status+project+completedAt at the index
|
|
321
|
+
// level so a single-project billing query is never a post-hoc filter
|
|
322
|
+
// over a truncated cross-project scan (same disease, same fix).
|
|
323
|
+
.index("by_status_project_completedAt", ["status", "project", "completedAt"])
|
|
324
|
+
// Day-132 live-defect fix: `updatedSince` on `tasks.list` was filtered
|
|
325
|
+
// IN-MEMORY after a fixed-size widened scan, so narrowing the window
|
|
326
|
+
// never reduced the number of candidates fetched and the scan cap bit
|
|
327
|
+
// into normal usage on exactly two branches (measured in production):
|
|
328
|
+
// `assignedTo` alone, and `assignedTo` + `status`. These two compound
|
|
329
|
+
// indexes end in `updatedAt` so the bound lives in the query itself —
|
|
330
|
+
// no index added for missions/briefingNotes, whose branches were not
|
|
331
|
+
// measured to exceed the cap.
|
|
332
|
+
.index("by_assignee_updatedAt", ["assignedTo", "updatedAt"])
|
|
333
|
+
.index("by_assignee_status_updatedAt", ["assignedTo", "status", "updatedAt"])
|
|
334
|
+
// Day 159 — reciprocal unblock: find every task waiting on a given
|
|
335
|
+
// task so it can be swept back to "todo" the moment that task closes.
|
|
336
|
+
.index("by_blockedOnTaskId", ["blockedOnTaskId"])
|
|
337
|
+
// Day 102 v2.11.0 — CRUD baseline PR-C-bis option B (mission k575kc1r):
|
|
338
|
+
// Convex native BM25 search on task title, with filterFields for the
|
|
339
|
+
// common targeting axes (assignedTo, status, project, missionId).
|
|
340
|
+
.searchIndex("search_title", {
|
|
341
|
+
searchField: "title",
|
|
342
|
+
filterFields: ["assignedTo", "status", "project", "missionId", "orgId"],
|
|
343
|
+
}),
|
|
344
|
+
|
|
345
|
+
// ── diary ──────────────────────────────────────────────────────────────────
|
|
346
|
+
diary: defineTable({
|
|
347
|
+
date: v.string(), // "2026-03-25" ISO date
|
|
348
|
+
orchestrator: creatorValidator,
|
|
349
|
+
instanceId: v.optional(v.string()), // which instance wrote this
|
|
350
|
+
content: v.string(), // Full diary entry
|
|
351
|
+
highlights: v.optional(v.array(v.string())),
|
|
352
|
+
blockers: v.optional(v.array(v.string())),
|
|
353
|
+
// v2.4.8: auth-derived author captured at write time from oauthCtx.userId.
|
|
354
|
+
// Distinct from `orchestrator` (writer-intent label, client-supplied).
|
|
355
|
+
// Pre-v2.4.8 entries are backfilled with orchestrator as best-guess via
|
|
356
|
+
// migrations/diary_backfill_createdBy — NOT auth-verified.
|
|
357
|
+
createdBy: v.optional(creatorValidator),
|
|
358
|
+
createdAt: v.number(),
|
|
359
|
+
})
|
|
360
|
+
.index("by_orchestrator_date", ["orchestrator", "date"])
|
|
361
|
+
.index("by_date", ["date"]),
|
|
362
|
+
// by_createdBy_date intentionally omitted: createdBy filtering is handled
|
|
363
|
+
// as a universal post-take in-memory filter (mirrors tasks.ts:371-373 pattern).
|
|
364
|
+
// Adding an index pushdown optimization is deferred to a separate PR.
|
|
365
|
+
|
|
366
|
+
// ── briefingNotes ──────────────────────────────────────────────────────────
|
|
367
|
+
briefingNotes: defineTable({
|
|
368
|
+
title: v.string(),
|
|
369
|
+
topic: v.string(),
|
|
370
|
+
participants: v.array(v.string()), // ["pi", "laurent", "tau"]
|
|
371
|
+
content: v.string(), // Full briefing content
|
|
372
|
+
decisions: v.optional(v.array(v.string())),
|
|
373
|
+
linkedMemoryIds: v.optional(v.array(v.id("memories"))),
|
|
374
|
+
createdBy: creatorValidator,
|
|
375
|
+
createdAt: v.number(),
|
|
376
|
+
updatedAt: v.optional(v.number()), // set on first update
|
|
377
|
+
updatedBy: v.optional(creatorValidator), // orchestrator that last updated
|
|
378
|
+
// Beta multi-tenant scope. null/undefined = master (internal Alpha).
|
|
379
|
+
// Set to Clerk org slug (e.g. "acme-hr") for client-scoped rows.
|
|
380
|
+
orgId: v.optional(v.string()),
|
|
381
|
+
})
|
|
382
|
+
.index("by_topic", ["topic"])
|
|
383
|
+
.index("by_creator", ["createdBy", "createdAt"])
|
|
384
|
+
.index("by_orgId", ["orgId"])
|
|
385
|
+
// Day 102 v2.11.0 — CRUD baseline PR-C-bis option B (mission k575kc1r):
|
|
386
|
+
// Convex native BM25 search on briefing body, with filterFields for the
|
|
387
|
+
// common narrowing axes (topic, createdBy).
|
|
388
|
+
.searchIndex("search_content", {
|
|
389
|
+
searchField: "content",
|
|
390
|
+
filterFields: ["topic", "createdBy", "orgId"],
|
|
391
|
+
}),
|
|
392
|
+
|
|
393
|
+
// ── briefingNoteParticipants ──────────────────────────────────────────────
|
|
394
|
+
// Day 165 fix (task k175ga65p654z200ydj7s8qv5s8cnxfc): junction table so
|
|
395
|
+
// "is `identity` a participant on `noteId`" is an INDEX-RANGE predicate
|
|
396
|
+
// inside the get/list/search queries, never a post-query handler filter
|
|
397
|
+
// (R-11) and never an in-memory scan of `briefingNotes.participants`
|
|
398
|
+
// (Convex indexes can't range-query inside an array field's elements).
|
|
399
|
+
// One row per (noteId, participant) pair; kept in sync by create/update.
|
|
400
|
+
briefingNoteParticipants: defineTable({
|
|
401
|
+
noteId: v.id("briefingNotes"),
|
|
402
|
+
participant: v.string(),
|
|
403
|
+
})
|
|
404
|
+
.index("by_participant_note", ["participant", "noteId"])
|
|
405
|
+
.index("by_note", ["noteId"]),
|
|
406
|
+
|
|
407
|
+
// ── components ──────────────────────────────────────────────────────────
|
|
408
|
+
// Registry of agents, skills, hooks, plugins — backup + inventory.
|
|
409
|
+
// Content stores the full file so nothing is lost if filesystem is destroyed.
|
|
410
|
+
components: defineTable({
|
|
411
|
+
name: v.string(),
|
|
412
|
+
type: v.union(
|
|
413
|
+
v.literal("agent"),
|
|
414
|
+
v.literal("skill"),
|
|
415
|
+
v.literal("hook"),
|
|
416
|
+
v.literal("plugin"),
|
|
417
|
+
),
|
|
418
|
+
team: v.optional(v.string()), // e.g. "marketing", "development"
|
|
419
|
+
content: v.string(), // full file content
|
|
420
|
+
version: v.optional(v.string()),
|
|
421
|
+
project: v.optional(v.string()),
|
|
422
|
+
createdBy: creatorValidator,
|
|
423
|
+
createdAt: v.number(),
|
|
424
|
+
updatedAt: v.number(),
|
|
425
|
+
})
|
|
426
|
+
.index("by_type", ["type"])
|
|
427
|
+
.index("by_team", ["team", "type"])
|
|
428
|
+
.index("by_name_type", ["name", "type"]),
|
|
429
|
+
|
|
430
|
+
// ── mandates ──────────────────────────────────────────────────────────────
|
|
431
|
+
// Cross-orchestrator service requests. One orchestrator requests a service from another.
|
|
432
|
+
// Budget (token allocation) is agreed upfront; cost is recorded on settle.
|
|
433
|
+
mandates: defineTable({
|
|
434
|
+
requestedBy: creatorValidator, // who needs the service
|
|
435
|
+
fulfilledBy: creatorValidator, // who will provide the service
|
|
436
|
+
service: v.string(), // description of what is needed
|
|
437
|
+
budget: v.number(), // token budget allocated
|
|
438
|
+
status: v.union(
|
|
439
|
+
v.literal("requested"),
|
|
440
|
+
v.literal("accepted"),
|
|
441
|
+
v.literal("in_progress"),
|
|
442
|
+
v.literal("delivered"),
|
|
443
|
+
v.literal("settled"),
|
|
444
|
+
),
|
|
445
|
+
linkedTaskIds: v.optional(v.array(v.id("tasks"))), // tasks created to fulfill this mandate
|
|
446
|
+
tokensCost: v.optional(v.number()), // actual tokens consumed
|
|
447
|
+
createdAt: v.number(),
|
|
448
|
+
updatedAt: v.number(),
|
|
449
|
+
completedAt: v.optional(v.number()),
|
|
450
|
+
// AP2 authorization fields
|
|
451
|
+
spendingLimits: v.optional(
|
|
452
|
+
v.object({
|
|
453
|
+
maxPerTransaction: v.number(),
|
|
454
|
+
maxPerPeriod: v.number(),
|
|
455
|
+
periodDays: v.optional(v.number()), // default 30
|
|
456
|
+
}),
|
|
457
|
+
),
|
|
458
|
+
approvedCategories: v.optional(v.array(v.string())), // e.g. ["seo", "content", "development"]
|
|
459
|
+
mandateDocument: v.optional(v.string()), // signed authorization text or reference
|
|
460
|
+
})
|
|
461
|
+
.index("by_requestedBy", ["requestedBy", "status"])
|
|
462
|
+
.index("by_fulfilledBy", ["fulfilledBy", "status"])
|
|
463
|
+
.index("by_status", ["status", "createdAt"]),
|
|
464
|
+
|
|
465
|
+
// ── issues ────────────────────────────────────────────────────────────────
|
|
466
|
+
// GitHub issues tracked in VantagePeers. Synced via webhook.
|
|
467
|
+
issues: defineTable({
|
|
468
|
+
repo: v.string(), // "myreeldream-ai/MyShortReel-beta"
|
|
469
|
+
issueNumber: v.number(),
|
|
470
|
+
title: v.string(),
|
|
471
|
+
body: v.string(), // truncated to 2000 chars
|
|
472
|
+
htmlUrl: v.string(),
|
|
473
|
+
labels: v.array(v.string()),
|
|
474
|
+
status: v.union(
|
|
475
|
+
v.literal("open"),
|
|
476
|
+
v.literal("in_progress"),
|
|
477
|
+
v.literal("fixed"),
|
|
478
|
+
v.literal("verified"),
|
|
479
|
+
v.literal("closed"),
|
|
480
|
+
),
|
|
481
|
+
priority: v.union(
|
|
482
|
+
v.literal("urgent"),
|
|
483
|
+
v.literal("high"),
|
|
484
|
+
v.literal("medium"),
|
|
485
|
+
v.literal("low"),
|
|
486
|
+
),
|
|
487
|
+
assignedOrchestrator: v.string(),
|
|
488
|
+
project: v.string(),
|
|
489
|
+
fixCommits: v.optional(v.array(v.string())),
|
|
490
|
+
fixedBy: v.optional(v.string()),
|
|
491
|
+
fixedAt: v.optional(v.number()),
|
|
492
|
+
verifiedBy: v.optional(v.string()),
|
|
493
|
+
verifiedAt: v.optional(v.number()),
|
|
494
|
+
linkedTaskIds: v.optional(v.array(v.string())),
|
|
495
|
+
githubCreatedAt: v.number(),
|
|
496
|
+
githubUpdatedAt: v.number(),
|
|
497
|
+
// External repo tracking (for Zeta contributions to third-party repos)
|
|
498
|
+
externalRepo: v.optional(v.string()), // "get-convex/better-auth"
|
|
499
|
+
externalIssueNumber: v.optional(v.number()),
|
|
500
|
+
externalIssueUrl: v.optional(v.string()),
|
|
501
|
+
prUrl: v.optional(v.string()),
|
|
502
|
+
prStatus: v.optional(
|
|
503
|
+
v.union(
|
|
504
|
+
v.literal("draft"),
|
|
505
|
+
v.literal("open"),
|
|
506
|
+
v.literal("merged"),
|
|
507
|
+
v.literal("closed"),
|
|
508
|
+
),
|
|
509
|
+
),
|
|
510
|
+
forkRepo: v.optional(v.string()), // "elpiarthera/better-auth"
|
|
511
|
+
})
|
|
512
|
+
.index("by_repo_number", ["repo", "issueNumber"])
|
|
513
|
+
.index("by_status", ["status"])
|
|
514
|
+
.index("by_project", ["project"])
|
|
515
|
+
.index("by_assigned", ["assignedOrchestrator", "status"])
|
|
516
|
+
.index("by_external_repo", ["externalRepo", "prStatus"]),
|
|
517
|
+
|
|
518
|
+
// ── githubRepoMapping ─────────────────────────────────────────────────────
|
|
519
|
+
// Maps GitHub repos to orchestrators. Used by webhook handler to route events.
|
|
520
|
+
//
|
|
521
|
+
// Day 98 (k173yr5n1) — auto-IRP cascade overhaul Mechanism (a):
|
|
522
|
+
// lastDeployedSHA + lastDeployedAt track the most recent successful Convex
|
|
523
|
+
// deploy of the repo. createDeployTaskWithDedup compares pr.mergedAt vs
|
|
524
|
+
// lastDeployedAt — if a deploy completed AFTER the PR merged, the PR is
|
|
525
|
+
// already shipped via a bundled chain and no per-PR Deploy task is spawned.
|
|
526
|
+
// Recorded via githubRepoMapping.recordDeployment mutation after each
|
|
527
|
+
// successful `npx convex deploy --yes`.
|
|
528
|
+
githubRepoMapping: defineTable({
|
|
529
|
+
repo: v.string(), // "myreeldream-ai/MyShortReel-beta"
|
|
530
|
+
orchestrator: v.string(), // "omega", "tau", "sigma", etc.
|
|
531
|
+
project: v.string(), // "myreeldream", "vantage-starter", etc.
|
|
532
|
+
active: v.boolean(),
|
|
533
|
+
lastDeployedSHA: v.optional(v.string()), // Day 98 — most recent prod-deployed commit OID
|
|
534
|
+
lastDeployedAt: v.optional(v.number()), // Day 98 — Unix ms timestamp of that deploy
|
|
535
|
+
}).index("by_repo", ["repo"]),
|
|
536
|
+
|
|
537
|
+
// ── businessUnits ─────────────────────────────────────────────────────────
|
|
538
|
+
// One row per ElPi Corp business unit. Tracks strategy, structure, and KPIs.
|
|
539
|
+
// managementFee: ElPi Corp takes this percentage of revenue (default 10%).
|
|
540
|
+
businessUnits: defineTable({
|
|
541
|
+
name: v.string(), // e.g. "VantagePeers", "VantageRegistry"
|
|
542
|
+
description: v.string(),
|
|
543
|
+
purpose: v.string(),
|
|
544
|
+
domain: v.optional(v.string()), // e.g. "vantagepeers.com"
|
|
545
|
+
orchestratorId: v.string(), // lead orchestrator — e.g. "sigma"
|
|
546
|
+
status: v.union(
|
|
547
|
+
v.literal("idea"),
|
|
548
|
+
v.literal("building"),
|
|
549
|
+
v.literal("live"),
|
|
550
|
+
v.literal("revenue"),
|
|
551
|
+
),
|
|
552
|
+
businessModel: v.string(), // how it makes money
|
|
553
|
+
targetCustomers: v.string(),
|
|
554
|
+
services: v.array(v.string()), // what it offers
|
|
555
|
+
pricing: v.string(),
|
|
556
|
+
revenueProjections: v.object({
|
|
557
|
+
y1: v.number(),
|
|
558
|
+
y2: v.number(),
|
|
559
|
+
y3: v.number(),
|
|
560
|
+
}),
|
|
561
|
+
coreTeam: v.object({
|
|
562
|
+
agents: v.array(v.string()),
|
|
563
|
+
skills: v.array(v.string()),
|
|
564
|
+
hooks: v.array(v.string()),
|
|
565
|
+
plugins: v.array(v.string()),
|
|
566
|
+
}),
|
|
567
|
+
coreProcesses: v.array(v.string()),
|
|
568
|
+
dependencies: v.array(v.string()), // which BUs it consumes
|
|
569
|
+
kpis: v.array(v.string()),
|
|
570
|
+
managementFee: v.number(), // ElPi Corp % cut (default 10)
|
|
571
|
+
createdAt: v.number(),
|
|
572
|
+
updatedAt: v.number(),
|
|
573
|
+
})
|
|
574
|
+
.index("by_orchestrator", ["orchestratorId"])
|
|
575
|
+
.index("by_status", ["status"])
|
|
576
|
+
.index("by_name", ["name"]),
|
|
577
|
+
|
|
578
|
+
// ── recurringTasks ────────────────────────────────────────────────────────
|
|
579
|
+
// Templates for tasks that auto-create on a schedule (daily standup, weekly scan, etc.)
|
|
580
|
+
// Convex cron checks every 15 min and creates tasks when nextRunAt <= now.
|
|
581
|
+
recurringTasks: defineTable({
|
|
582
|
+
title: v.string(),
|
|
583
|
+
description: v.optional(v.string()),
|
|
584
|
+
// Orchestrator or person assigned — open string (issue #132)
|
|
585
|
+
assignedTo: v.string(),
|
|
586
|
+
priority: v.union(
|
|
587
|
+
v.literal("urgent"),
|
|
588
|
+
v.literal("high"),
|
|
589
|
+
v.literal("medium"),
|
|
590
|
+
v.literal("low"),
|
|
591
|
+
),
|
|
592
|
+
project: v.optional(v.string()),
|
|
593
|
+
tags: v.optional(v.array(v.string())),
|
|
594
|
+
cronExpression: v.string(), // "0 9 * * *" = daily 9am, "0 9 * * 1" = Monday 9am
|
|
595
|
+
lastCreatedAt: v.optional(v.number()),
|
|
596
|
+
nextRunAt: v.number(),
|
|
597
|
+
active: v.boolean(),
|
|
598
|
+
createdBy: creatorValidator,
|
|
599
|
+
createdAt: v.number(),
|
|
600
|
+
updatedAt: v.number(),
|
|
601
|
+
})
|
|
602
|
+
.index("by_active", ["active", "nextRunAt"])
|
|
603
|
+
.index("by_assignee", ["assignedTo"]),
|
|
604
|
+
|
|
605
|
+
// ── fixPatterns ──────────────────────────────────────────────────────────────
|
|
606
|
+
// Knowledge base of fix patterns. Documents bugs, root causes, fix attempts
|
|
607
|
+
// (including failures), and validated fixes. Agents search this BEFORE fixing
|
|
608
|
+
// to avoid repeating mistakes. Semantic search via RAG on symptom + rootCause.
|
|
609
|
+
fixPatterns: defineTable({
|
|
610
|
+
symptom: v.string(), // What the bug looks like (searchable via RAG)
|
|
611
|
+
rootCause: v.string(), // Why it happens
|
|
612
|
+
validatedFix: v.optional(v.string()), // The fix that actually worked
|
|
613
|
+
files: v.optional(v.array(v.string())), // Files involved
|
|
614
|
+
tags: v.array(v.string()), // e.g. "react-hydration", "convex-subscription"
|
|
615
|
+
stack: v.array(v.string()), // e.g. "next.js", "convex", "clerk"
|
|
616
|
+
sourceProject: v.string(), // Origin project
|
|
617
|
+
linkedIssueIds: v.optional(v.array(v.string())), // VantagePeers issue IDs
|
|
618
|
+
createdBy: creatorValidator,
|
|
619
|
+
severity: severityValidator,
|
|
620
|
+
createdAt: v.number(),
|
|
621
|
+
updatedAt: v.number(),
|
|
622
|
+
})
|
|
623
|
+
.index("by_project", ["sourceProject"])
|
|
624
|
+
.index("by_severity", ["severity"])
|
|
625
|
+
.index("by_creator", ["createdBy"]),
|
|
626
|
+
|
|
627
|
+
// ── fixAttempts ──────────────────────────────────────────────────────────────
|
|
628
|
+
// Individual fix attempts for a pattern. Separate table to avoid unbounded
|
|
629
|
+
// array growth in fixPatterns documents (per Convex guidelines).
|
|
630
|
+
fixAttempts: defineTable({
|
|
631
|
+
patternId: v.id("fixPatterns"),
|
|
632
|
+
description: v.string(), // What was tried
|
|
633
|
+
commit: v.optional(v.string()), // Commit hash
|
|
634
|
+
worked: v.boolean(), // Did it fix the issue?
|
|
635
|
+
why: v.string(), // Why it worked or didn't
|
|
636
|
+
createdBy: creatorValidator,
|
|
637
|
+
createdAt: v.number(),
|
|
638
|
+
})
|
|
639
|
+
.index("by_pattern", ["patternId"])
|
|
640
|
+
.index("by_worked", ["patternId", "worked"]),
|
|
641
|
+
|
|
642
|
+
// ── missionTemplates ──────────────────────────────────────────────────────
|
|
643
|
+
// Reusable mission templates. Each template contains an ordered list of
|
|
644
|
+
// steps that get instantiated as tasks when a triggering event occurs.
|
|
645
|
+
// The "issue-resolution-v2" template drives the IRP (Issue Resolution Protocol).
|
|
646
|
+
missionTemplates: defineTable({
|
|
647
|
+
name: v.string(),
|
|
648
|
+
description: v.optional(v.string()),
|
|
649
|
+
brief: v.optional(v.string()), // Reused as the mission's brief on instantiation when the caller supplies none.
|
|
650
|
+
steps: v.array(
|
|
651
|
+
v.object({
|
|
652
|
+
title: v.string(),
|
|
653
|
+
description: v.string(),
|
|
654
|
+
tags: v.optional(v.array(v.string())),
|
|
655
|
+
assignedTo: v.optional(v.string()), // orchestrator role, e.g. "proxima"
|
|
656
|
+
assignedToInstance: v.optional(v.string()), // instance, e.g. "proxima-vps"
|
|
657
|
+
dependsOn: v.optional(v.array(v.number())), // indexes of required steps (0-based)
|
|
658
|
+
}),
|
|
659
|
+
),
|
|
660
|
+
isDefault: v.boolean(),
|
|
661
|
+
createdBy: creatorValidator,
|
|
662
|
+
createdAt: v.number(),
|
|
663
|
+
updatedAt: v.number(),
|
|
664
|
+
// Soft-delete marker — mirrors memories.softDeleteMemory's audit-preserving
|
|
665
|
+
// PATCH-a-flag motif. Chose a timestamp (not a boolean like memories'
|
|
666
|
+
// `isLatest`) because it also records *when* the template was retired,
|
|
667
|
+
// which the memories table gets for free via `updatedAt` alongside the
|
|
668
|
+
// flag — a lone boolean here would lose that provenance. undefined = active.
|
|
669
|
+
deletedAt: v.optional(v.number()),
|
|
670
|
+
})
|
|
671
|
+
.index("by_name", ["name"])
|
|
672
|
+
.index("by_default", ["isDefault"]),
|
|
673
|
+
|
|
674
|
+
// ── monitoredDeployments ─────────────────────────────────────────────────
|
|
675
|
+
// Registry of Convex deployments to poll for errors.
|
|
676
|
+
// deployKeyEnvVar: name of the env var holding the admin deploy key.
|
|
677
|
+
monitoredDeployments: defineTable({
|
|
678
|
+
name: v.string(),
|
|
679
|
+
deploymentUrl: v.string(),
|
|
680
|
+
deployKeyEnvVar: v.string(),
|
|
681
|
+
githubRepo: v.string(),
|
|
682
|
+
orchestrator: v.string(),
|
|
683
|
+
active: v.boolean(),
|
|
684
|
+
lastCursor: v.optional(v.number()),
|
|
685
|
+
createdAt: v.number(),
|
|
686
|
+
})
|
|
687
|
+
.index("by_active", ["active"])
|
|
688
|
+
.index("by_name", ["name"]),
|
|
689
|
+
|
|
690
|
+
// ── issueStats ───────────────────────────────────────────────────────────
|
|
691
|
+
// Daily issue resolution metrics per repo.
|
|
692
|
+
issueStats: defineTable({
|
|
693
|
+
repo: v.string(),
|
|
694
|
+
date: v.string(), // YYYY-MM-DD
|
|
695
|
+
totalIssues: v.number(),
|
|
696
|
+
resolvedIssues: v.number(),
|
|
697
|
+
medianTimeToFirstResponse: v.optional(v.number()), // minutes
|
|
698
|
+
medianTimeToFix: v.optional(v.number()), // minutes
|
|
699
|
+
fastestResolution: v.optional(v.number()), // minutes
|
|
700
|
+
slowestResolution: v.optional(v.number()), // minutes
|
|
701
|
+
avgTimeToFix: v.optional(v.number()), // minutes
|
|
702
|
+
// Before/after VantageOS Team (pivot: 2026-04-01)
|
|
703
|
+
beforeVantageOS: v.optional(
|
|
704
|
+
v.object({
|
|
705
|
+
totalIssues: v.number(),
|
|
706
|
+
resolvedIssues: v.number(),
|
|
707
|
+
medianTimeToFix: v.optional(v.number()),
|
|
708
|
+
avgTimeToFix: v.optional(v.number()),
|
|
709
|
+
}),
|
|
710
|
+
),
|
|
711
|
+
afterVantageOS: v.optional(
|
|
712
|
+
v.object({
|
|
713
|
+
totalIssues: v.number(),
|
|
714
|
+
resolvedIssues: v.number(),
|
|
715
|
+
medianTimeToFix: v.optional(v.number()),
|
|
716
|
+
avgTimeToFix: v.optional(v.number()),
|
|
717
|
+
}),
|
|
718
|
+
),
|
|
719
|
+
issueDetails: v.optional(
|
|
720
|
+
v.array(
|
|
721
|
+
v.object({
|
|
722
|
+
number: v.number(),
|
|
723
|
+
title: v.string(),
|
|
724
|
+
timeToFirstResponse: v.optional(v.number()),
|
|
725
|
+
timeToFix: v.optional(v.number()),
|
|
726
|
+
status: v.string(),
|
|
727
|
+
}),
|
|
728
|
+
),
|
|
729
|
+
),
|
|
730
|
+
calculatedAt: v.number(),
|
|
731
|
+
})
|
|
732
|
+
.index("by_repo_date", ["repo", "date"])
|
|
733
|
+
.index("by_date", ["date"]),
|
|
734
|
+
|
|
735
|
+
// ── kbUploads ─────────────────────────────────────────────────────────────
|
|
736
|
+
// TOFU (trust-on-first-use) storage ownership binding table.
|
|
737
|
+
// Created by B5 M1 storageId org-binding defense-in-depth (mission k5779q).
|
|
738
|
+
//
|
|
739
|
+
// On first call to storeDocumentChunked for a given storageId, an entry is
|
|
740
|
+
// inserted binding that storageId to the calling org. Subsequent calls with
|
|
741
|
+
// the SAME storageId but a DIFFERENT orgId are rejected with
|
|
742
|
+
// AUTH_STORAGE_NOT_OWNED to close the cross-tenant attack vector identified
|
|
743
|
+
// by Eta iter-2 post-merge follow-up (PR #992, commit 16bb32d).
|
|
744
|
+
//
|
|
745
|
+
// Index: by_storageId — O(1) ownership lookup before ctx.storage.get().
|
|
746
|
+
kbUploads: defineTable({
|
|
747
|
+
storageId: v.id("_storage"),
|
|
748
|
+
orgId: v.string(),
|
|
749
|
+
createdAt: v.number(),
|
|
750
|
+
}).index("by_storageId", ["storageId"]),
|
|
751
|
+
|
|
752
|
+
// ── oauth_clients ─────────────────────────────────────────────────────────
|
|
753
|
+
// OAuth 2.0 Dynamic Client Registration (RFC 7591).
|
|
754
|
+
// One row per registered OAuth client (Claude.ai custom connector, Nadia,
|
|
755
|
+
// future Perello VIP clients). Each client is bound to a scopeProfile that
|
|
756
|
+
// limits the `from=` allowlist and namespace read/write prefixes.
|
|
757
|
+
//
|
|
758
|
+
// clientSecretHash = SHA-256 hex of the raw client secret (raw never stored).
|
|
759
|
+
// Admin onboarding returns the raw secret exactly ONCE to the provisioner.
|
|
760
|
+
oauth_clients: defineTable({
|
|
761
|
+
clientId: v.string(), // public identifier (UUID)
|
|
762
|
+
clientSecretHash: v.string(), // SHA-256 hex of raw secret
|
|
763
|
+
redirectUris: v.array(v.string()),
|
|
764
|
+
name: v.string(), // human label, e.g. "nadia-acme-hr"
|
|
765
|
+
scopeProfile: v.string(), // FK to oauth_scope_profiles.profileId
|
|
766
|
+
createdAt: v.number(),
|
|
767
|
+
revokedAt: v.optional(v.number()),
|
|
768
|
+
// RFC 7591 §2 token_endpoint_auth_method (D6 — confidential client validation).
|
|
769
|
+
// Absent => treat as confidential ("client_secret_basic") for backward
|
|
770
|
+
// compatibility; existing rows MUST be backfilled in S2 with explicit value
|
|
771
|
+
// ("client_secret_basic" for confidential; "none" for known public clients).
|
|
772
|
+
tokenEndpointAuthMethod: v.optional(v.string()),
|
|
773
|
+
})
|
|
774
|
+
.index("by_clientId", ["clientId"])
|
|
775
|
+
.index("by_scopeProfile", ["scopeProfile"]),
|
|
776
|
+
|
|
777
|
+
// ── oauth_authorization_codes ────────────────────────────────────────────
|
|
778
|
+
// Short-lived authorization codes (RFC 6749 §4.1). TTL ~10 min.
|
|
779
|
+
// Cleaned periodically by cron (processExpiredOauthCodes).
|
|
780
|
+
oauth_authorization_codes: defineTable({
|
|
781
|
+
code: v.string(), // opaque random value
|
|
782
|
+
clientId: v.string(),
|
|
783
|
+
redirectUri: v.string(),
|
|
784
|
+
codeChallenge: v.string(), // PKCE S256 (RFC 7636)
|
|
785
|
+
scope: v.string(),
|
|
786
|
+
userId: v.string(), // who the code was issued for (== scopeProfile owner by default)
|
|
787
|
+
expiresAt: v.number(),
|
|
788
|
+
}).index("by_code", ["code"]),
|
|
789
|
+
|
|
790
|
+
// ── oauth_access_tokens ──────────────────────────────────────────────────
|
|
791
|
+
// Issued access tokens. tokenHash = SHA-256 hex of the raw token (raw never
|
|
792
|
+
// stored). The HTTP bearer middleware hashes the incoming Authorization
|
|
793
|
+
// header and looks the token up by hash, then enforces fromAllowList +
|
|
794
|
+
// namespaceRead/Write prefixes on every MCP tool call.
|
|
795
|
+
oauth_access_tokens: defineTable({
|
|
796
|
+
tokenHash: v.string(),
|
|
797
|
+
clientId: v.string(),
|
|
798
|
+
userId: v.string(),
|
|
799
|
+
scopes: v.array(v.string()), // OAuth scopes (vantage:read, vantage:write)
|
|
800
|
+
scopeProfile: v.string(), // snapshot at issue time
|
|
801
|
+
fromAllowList: v.array(v.string()),
|
|
802
|
+
namespaceReadPrefixes: v.array(v.string()),
|
|
803
|
+
namespaceWritePrefixes: v.array(v.string()),
|
|
804
|
+
expiresAt: v.number(),
|
|
805
|
+
refreshTokenHash: v.optional(v.string()),
|
|
806
|
+
createdAt: v.number(),
|
|
807
|
+
revokedAt: v.optional(v.number()),
|
|
808
|
+
})
|
|
809
|
+
.index("by_tokenHash", ["tokenHash"])
|
|
810
|
+
.index("by_clientId", ["clientId"]),
|
|
811
|
+
|
|
812
|
+
// ── oauth_refresh_tokens ─────────────────────────────────────────────────
|
|
813
|
+
// Refresh tokens for renewing expired access tokens. Same hashing rules.
|
|
814
|
+
oauth_refresh_tokens: defineTable({
|
|
815
|
+
tokenHash: v.string(),
|
|
816
|
+
clientId: v.string(),
|
|
817
|
+
userId: v.string(),
|
|
818
|
+
scopeProfile: v.string(),
|
|
819
|
+
expiresAt: v.number(),
|
|
820
|
+
createdAt: v.number(),
|
|
821
|
+
revokedAt: v.optional(v.number()),
|
|
822
|
+
})
|
|
823
|
+
.index("by_tokenHash", ["tokenHash"])
|
|
824
|
+
.index("by_clientId", ["clientId"]),
|
|
825
|
+
|
|
826
|
+
// ── oauth_scope_profiles ─────────────────────────────────────────────────
|
|
827
|
+
// Reusable scope templates. An OAuth client references a profile by
|
|
828
|
+
// profileId; the access_token materialises the profile into the token row
|
|
829
|
+
// so scope changes on the profile don't retroactively grant or revoke.
|
|
830
|
+
//
|
|
831
|
+
// Seeded on first deploy via oauth.seedDefaultProfiles: master, nadia-acme-hr,
|
|
832
|
+
// client-generic (deny-by-default template).
|
|
833
|
+
oauth_scope_profiles: defineTable({
|
|
834
|
+
profileId: v.string(), // stable slug e.g. "nadia-acme-hr"
|
|
835
|
+
description: v.string(),
|
|
836
|
+
fromAllowList: v.array(v.string()),
|
|
837
|
+
namespaceReadPrefixes: v.array(v.string()),
|
|
838
|
+
namespaceWritePrefixes: v.array(v.string()),
|
|
839
|
+
createdAt: v.number(),
|
|
840
|
+
updatedAt: v.number(),
|
|
841
|
+
}).index("by_profileId", ["profileId"]),
|
|
842
|
+
|
|
843
|
+
// ── mcpTenants ────────────────────────────────────────────────────────────
|
|
844
|
+
// Registry of VIP tenants for HTTP MCP transport.
|
|
845
|
+
// Each tenant has a hashed bearer token and a target Convex deployment URL.
|
|
846
|
+
// The HTTP MCP server (Railway) looks up tenants by tokenHash on every request
|
|
847
|
+
// and proxies to the correct Convex deployment.
|
|
848
|
+
// tokenHash = SHA-256 hex of the raw bearer token (raw token never stored).
|
|
849
|
+
mcpTenants: defineTable({
|
|
850
|
+
tokenHash: v.string(), // SHA-256 hex of bearer token
|
|
851
|
+
tenantName: v.string(), // e.g. "perello-consulting-vip-1"
|
|
852
|
+
convexUrl: v.string(), // e.g. "https://xxxx.convex.cloud"
|
|
853
|
+
createdAt: v.number(),
|
|
854
|
+
enabledAt: v.optional(v.number()), // undefined = disabled
|
|
855
|
+
lastUsedAt: v.optional(v.number()),
|
|
856
|
+
revokedAt: v.optional(v.number()),
|
|
857
|
+
}).index("by_tokenHash", ["tokenHash"]),
|
|
858
|
+
|
|
859
|
+
// ── errorMonitorFilterRules ──────────────────────────────────────────────
|
|
860
|
+
// Runtime-configurable filter rules for the auto-IRP bot.
|
|
861
|
+
// Each rule matches (functionName, errorMessageRegex) and assigns a severity:
|
|
862
|
+
// "skip" → drop silently, no upsert / issue
|
|
863
|
+
// "log-only" → console.log only
|
|
864
|
+
// "create-issue" → normal flow (used to override a default skip)
|
|
865
|
+
//
|
|
866
|
+
// Defaults are seeded by errorMonitorFilters.seedDefaultRules. Sigma/Pi
|
|
867
|
+
// can add or disable rules without redeploying via addFilterRule /
|
|
868
|
+
// disableFilterRule — internalMutation (admin-only, invoked with a deploy
|
|
869
|
+
// key via `npx convex run`; converted from public to close a DoS-adjacent
|
|
870
|
+
// suppression surface, see errorMonitorFilters.ts).
|
|
871
|
+
//
|
|
872
|
+
// Linked: memory j573cwcs3znp0xsvtg34x435jh84b0eg, pattern m978zeg4b2e9nx67z2hg5rwgfs85hf7f.
|
|
873
|
+
errorMonitorFilterRules: defineTable({
|
|
874
|
+
functionName: v.string(), // exact match, e.g. "tasks:complete"
|
|
875
|
+
errorMessageRegex: v.string(), // RegExp source
|
|
876
|
+
regexFlags: v.optional(v.string()), // e.g. "i"
|
|
877
|
+
reason: v.string(), // human-readable why this is filtered
|
|
878
|
+
severity: v.union(
|
|
879
|
+
v.literal("skip"),
|
|
880
|
+
v.literal("log-only"),
|
|
881
|
+
v.literal("create-issue"),
|
|
882
|
+
),
|
|
883
|
+
active: v.boolean(),
|
|
884
|
+
createdAt: v.number(),
|
|
885
|
+
// v1.0.1 observability — added in PR follow-up to PR #354 review.
|
|
886
|
+
// `lastMatchedAt` and `matchCount` are bumped from `pollDeploymentLogs`
|
|
887
|
+
// via a fire-and-forget mutation when a rule's severity is "skip" or
|
|
888
|
+
// "log-only" (the silenced classes), so operators can see which rules
|
|
889
|
+
// are actually firing in prod and which are dead weight.
|
|
890
|
+
lastMatchedAt: v.optional(v.number()), // Unix ms of last match
|
|
891
|
+
matchCount: v.optional(v.number()), // running counter, treat undefined as 0
|
|
892
|
+
// v1.0.1 precedence — higher `priority` = evaluated first. Stable sort
|
|
893
|
+
// by `_creationTime` for ties. Treat undefined as 0 (unprioritized).
|
|
894
|
+
priority: v.optional(v.number()),
|
|
895
|
+
})
|
|
896
|
+
.index("by_active", ["active"])
|
|
897
|
+
.index("by_function", ["functionName", "active"]),
|
|
898
|
+
|
|
899
|
+
// ── oauthClients ─────────────────────────────────────────────────────────
|
|
900
|
+
// OAuth 2.1 Dynamic Client Registration (RFC 7591) clients.
|
|
901
|
+
// Issued by the DCR /register endpoint without admin gating.
|
|
902
|
+
// clientSecret = 64-char hex (raw value — returned once on registration,
|
|
903
|
+
// stored here for PKCE / client_secret_post auth on the token endpoint).
|
|
904
|
+
oauthClients: defineTable({
|
|
905
|
+
clientId: v.string(), // crypto.randomUUID()
|
|
906
|
+
clientSecret: v.string(), // 64-char hex (raw — transmitted once)
|
|
907
|
+
clientName: v.string(),
|
|
908
|
+
redirectUris: v.array(v.string()),
|
|
909
|
+
createdAt: v.number(),
|
|
910
|
+
scope: v.optional(v.string()), // default "mcp:full"
|
|
911
|
+
}).index("by_clientId", ["clientId"]),
|
|
912
|
+
|
|
913
|
+
// ── oauthTokens ──────────────────────────────────────────────────────────
|
|
914
|
+
// Auth codes and access/refresh tokens for the DCR OAuth 2.1 flow.
|
|
915
|
+
// A single row covers both the auth-code phase (authCode set, accessToken
|
|
916
|
+
// absent) and the token phase (accessToken set, authCode consumed).
|
|
917
|
+
// Index on authCode supports code exchange; index on accessToken supports
|
|
918
|
+
// bearer validation; index on clientId supports revocation/listing.
|
|
919
|
+
oauthTokens: defineTable({
|
|
920
|
+
clientId: v.string(),
|
|
921
|
+
accessToken: v.string(), // crypto.randomUUID()
|
|
922
|
+
refreshToken: v.optional(v.string()),
|
|
923
|
+
scope: v.string(),
|
|
924
|
+
expiresAt: v.number(), // ms since epoch
|
|
925
|
+
authCode: v.optional(v.string()), // PKCE authorization code
|
|
926
|
+
codeChallenge: v.optional(v.string()),
|
|
927
|
+
codeChallengeMethod: v.optional(v.string()),
|
|
928
|
+
redirectUri: v.optional(v.string()),
|
|
929
|
+
used: v.optional(v.boolean()), // auth code single-use flag
|
|
930
|
+
createdAt: v.number(),
|
|
931
|
+
})
|
|
932
|
+
.index("by_accessToken", ["accessToken"])
|
|
933
|
+
.index("by_authCode", ["authCode"])
|
|
934
|
+
.index("by_clientId", ["clientId"])
|
|
935
|
+
.index("by_refreshToken", ["refreshToken"]),
|
|
936
|
+
|
|
937
|
+
// ── errorLogs ────────────────────────────────────────────────────────────
|
|
938
|
+
// Deduplicated log of detected function errors across monitored deployments.
|
|
939
|
+
// hash = simpleHash(functionName + ":" + errorMessage) for deduplication.
|
|
940
|
+
//
|
|
941
|
+
// Anti-flood additions (Day 76):
|
|
942
|
+
// issueCreated — true once the GH issue + IRP mission have been created.
|
|
943
|
+
// Prevents re-triggering if the errorLog is touched again.
|
|
944
|
+
// irpMissionId — ID of the auto-IRP mission spawned for this error, so
|
|
945
|
+
// the auto-resolver can cascade-close it.
|
|
946
|
+
// autoResolved — true once the auto-resolver has closed the mission.
|
|
947
|
+
// Prevents the resolver from double-processing.
|
|
948
|
+
// recurrenceThreshold — overrides the deployment-level default; the GH issue
|
|
949
|
+
// is NOT created until count >= this value.
|
|
950
|
+
errorLogs: defineTable({
|
|
951
|
+
hash: v.string(),
|
|
952
|
+
deployment: v.string(),
|
|
953
|
+
functionName: v.string(),
|
|
954
|
+
errorMessage: v.string(),
|
|
955
|
+
stackTrace: v.optional(v.string()),
|
|
956
|
+
firstSeen: v.number(),
|
|
957
|
+
lastSeen: v.number(),
|
|
958
|
+
count: v.number(),
|
|
959
|
+
issueNumber: v.optional(v.number()),
|
|
960
|
+
githubRepo: v.optional(v.string()),
|
|
961
|
+
// Day 76 anti-flood fields
|
|
962
|
+
issueCreated: v.optional(v.boolean()),
|
|
963
|
+
irpMissionId: v.optional(v.id("missions")),
|
|
964
|
+
autoResolved: v.optional(v.boolean()),
|
|
965
|
+
recurrenceThreshold: v.optional(v.number()),
|
|
966
|
+
// Day 128 fix (issue #1088 fabricated incident) — see
|
|
967
|
+
// convex/errorMonitorRecurrence.ts. Records the `count` value at the
|
|
968
|
+
// moment a 24h+ re-raise measurement window was armed. The RECURRING
|
|
969
|
+
// escalation label is only emitted once NEW occurrences counted after
|
|
970
|
+
// this baseline reach the effective threshold — never from group
|
|
971
|
+
// identity + a stale timestamp alone.
|
|
972
|
+
reRaiseBaselineCount: v.optional(v.number()),
|
|
973
|
+
})
|
|
974
|
+
.index("by_hash", ["hash"])
|
|
975
|
+
.index("by_deployment", ["deployment"])
|
|
976
|
+
.index("by_issue_created", ["issueCreated", "lastSeen"])
|
|
977
|
+
.index("by_issue_number", ["issueNumber"]),
|
|
978
|
+
|
|
979
|
+
// ── licenses ─────────────────────────────────────────────────────────────
|
|
980
|
+
// Open-core license registry. Raw license keys are NEVER stored — only the
|
|
981
|
+
// SHA-256 hex hash is persisted. The raw key is returned once on generate and
|
|
982
|
+
// is the customer's responsibility to store safely.
|
|
983
|
+
//
|
|
984
|
+
// Lifecycle: generated → activated (optional, sets activatedAt) → active until
|
|
985
|
+
// expiresAt, after which status moves to "expired" (handled by cron or on
|
|
986
|
+
// validate). Revocation sets status="revoked" immediately.
|
|
987
|
+
licenses: defineTable({
|
|
988
|
+
keyHash: v.string(), // sha256 hex of the raw license key
|
|
989
|
+
customerEmail: v.string(),
|
|
990
|
+
customerName: v.optional(v.string()),
|
|
991
|
+
productCode: v.string(), // e.g. "vantage-peers-self-host"
|
|
992
|
+
tier: v.string(), // e.g. "open-core-99-eur-yr"
|
|
993
|
+
purchasedAt: v.number(), // ms since epoch
|
|
994
|
+
activatedAt: v.optional(v.number()), // ms since epoch, set on first activation
|
|
995
|
+
expiresAt: v.number(), // purchasedAt + 365d (ms since epoch)
|
|
996
|
+
gumroadOrderId: v.optional(v.string()),
|
|
997
|
+
status: v.union(
|
|
998
|
+
v.literal("active"),
|
|
999
|
+
v.literal("trial"),
|
|
1000
|
+
v.literal("revoked"),
|
|
1001
|
+
v.literal("expired"),
|
|
1002
|
+
),
|
|
1003
|
+
githubRepos: v.optional(v.array(v.string())),
|
|
1004
|
+
purchaseLocale: v.optional(v.union(v.literal("en"), v.literal("fr"))),
|
|
1005
|
+
// Onboarding email delivery tracking (set by gumroadWebhook handler)
|
|
1006
|
+
emailSent: v.optional(v.boolean()),
|
|
1007
|
+
// Customer segmentation — set at purchase time or enriched later.
|
|
1008
|
+
// Unset = unknown / backwards-compat for rows created before W3.
|
|
1009
|
+
customerType: v.optional(
|
|
1010
|
+
v.union(
|
|
1011
|
+
v.literal("early-dev-voie-3"),
|
|
1012
|
+
v.literal("smb-voie-2"),
|
|
1013
|
+
v.literal("pro-voie-3"),
|
|
1014
|
+
v.literal("partner-reseller"),
|
|
1015
|
+
v.literal("standard"),
|
|
1016
|
+
),
|
|
1017
|
+
),
|
|
1018
|
+
// True if the customer has expressed interest in reselling VantagePeers.
|
|
1019
|
+
resellerCandidate: v.optional(v.boolean()),
|
|
1020
|
+
})
|
|
1021
|
+
.index("by_keyHash", ["keyHash"])
|
|
1022
|
+
.index("by_customerEmail", ["customerEmail"])
|
|
1023
|
+
.index("by_gumroadOrderId", ["gumroadOrderId"]),
|
|
1024
|
+
|
|
1025
|
+
// ── iframeEmbedSessions ──────────────────────────────────────────────────
|
|
1026
|
+
// Session registry for VP Gen UI iframe embeds (SEP-1865 M3).
|
|
1027
|
+
// Each session is bound to an origin and carries optional tenantId + userId
|
|
1028
|
+
// for multi-tenant routing. Sessions expire via expiresAt; revoked flag
|
|
1029
|
+
// provides immediate invalidation without waiting for TTL.
|
|
1030
|
+
//
|
|
1031
|
+
// Mission : sigma-vantage-peers-mcp-gui-iframe-embed-v1 (k5730xct6rvrwkvxhy5t5js12d87jwfw).
|
|
1032
|
+
iframeEmbedSessions: defineTable({
|
|
1033
|
+
sessionId: v.string(),
|
|
1034
|
+
tenantId: v.optional(v.string()),
|
|
1035
|
+
origin: v.string(),
|
|
1036
|
+
userId: v.optional(v.string()),
|
|
1037
|
+
createdAt: v.number(),
|
|
1038
|
+
lastSeenAt: v.number(),
|
|
1039
|
+
expiresAt: v.number(),
|
|
1040
|
+
revoked: v.boolean(),
|
|
1041
|
+
})
|
|
1042
|
+
.index("by_session_id", ["sessionId"])
|
|
1043
|
+
.index("by_origin_expires", ["origin", "expiresAt"]),
|
|
1044
|
+
|
|
1045
|
+
// ── errorMonitorConfig ───────────────────────────────────────────────────
|
|
1046
|
+
// Singleton-ish dynamic configuration for the error-monitor subsystem.
|
|
1047
|
+
// Key-value store where `key` is a stable slug (e.g. "pendingAliasReleases")
|
|
1048
|
+
// and `value` is a string array.
|
|
1049
|
+
//
|
|
1050
|
+
// pendingAliasReleases — list of status aliases introduced in an in-flight
|
|
1051
|
+
// release that are not yet deployed to prod. The auto-IRP bot synthesises
|
|
1052
|
+
// ArgumentValidationError filter rules for each alias so that pre-deploy
|
|
1053
|
+
// smoke-test noise doesn't spawn GitHub issues. Remove aliases post-deploy.
|
|
1054
|
+
errorMonitorConfig: defineTable({
|
|
1055
|
+
key: v.string(), // e.g. "pendingAliasReleases"
|
|
1056
|
+
value: v.array(v.string()),
|
|
1057
|
+
updatedAt: v.number(),
|
|
1058
|
+
}).index("by_key", ["key"]),
|
|
1059
|
+
|
|
1060
|
+
// ── taskClosureConfig ────────────────────────────────────────────────────
|
|
1061
|
+
// Day 130 (k17dhcmzqafve1ayzvh833kf558ae019) — server-side closure gate
|
|
1062
|
+
// config. Key-value store, mirrors errorMonitorConfig pattern.
|
|
1063
|
+
// Doctrine: no-hardcoded-business-knowledge — billable project list and
|
|
1064
|
+
// stale-in-progress threshold live here, never as a code constant.
|
|
1065
|
+
//
|
|
1066
|
+
// Known keys:
|
|
1067
|
+
// "billableProjects" — string[] of `task.project` values that
|
|
1068
|
+
// require a machine-timestamped startedAt before closure (billing
|
|
1069
|
+
// source = actualMinutes derived from startedAt→completedAt).
|
|
1070
|
+
// Seeded with ["vantage-immo"] — add client projects here, not in code.
|
|
1071
|
+
// "staleInProgressThresholdMs" — number encoded as a single-element
|
|
1072
|
+
// string[] (["86400000"]) — age (ms) after which an in_progress task
|
|
1073
|
+
// is surfaced as staleInProgress in check_messages. Default 24h.
|
|
1074
|
+
taskClosureConfig: defineTable({
|
|
1075
|
+
key: v.string(),
|
|
1076
|
+
value: v.array(v.string()),
|
|
1077
|
+
updatedAt: v.number(),
|
|
1078
|
+
}).index("by_key", ["key"]),
|
|
1079
|
+
|
|
1080
|
+
// ── client_org_mapping ───────────────────────────────────────────────────
|
|
1081
|
+
// Dashboard Beta multi-tenant scope registry. One row per Clerk organisation
|
|
1082
|
+
// granted dashboard access. Provisioned manually by Pi / Laurent via Convex
|
|
1083
|
+
// dashboard or npx convex run after merge.
|
|
1084
|
+
//
|
|
1085
|
+
// clerkOrgSlug: the Clerk organisation slug (e.g. "acme-hr", "a-client-org").
|
|
1086
|
+
// Maps to `identity.organizationId ?? identity.organizationSlug` in Convex
|
|
1087
|
+
// auth helpers.
|
|
1088
|
+
// allowedOrchestrators: which orchestrator pilots/assignees this org can see.
|
|
1089
|
+
// ["*"] = master sentinel = all orchestrators (Laurent / internal use).
|
|
1090
|
+
// scopes: permission tokens granted to this org.
|
|
1091
|
+
// Known values: "view-own-tasks", "view-own-missions",
|
|
1092
|
+
// "view-orchestrator-summary", "view-stats-aggregated", "cross-tenant-read".
|
|
1093
|
+
// isActive: false = org is disabled (returns Forbidden) without deleting the row.
|
|
1094
|
+
//
|
|
1095
|
+
// Seed rows (post-merge, Sigma runs npx convex run):
|
|
1096
|
+
// acme-hr → allowedOrchestrators=["victor"], scopes=["view-own-tasks","view-own-missions","view-orchestrator-summary"]
|
|
1097
|
+
// <redacted-client> → allowedOrchestrators=["phi"], scopes=["view-own-tasks","view-own-missions"]
|
|
1098
|
+
client_org_mapping: defineTable({
|
|
1099
|
+
clerkOrgSlug: v.string(), // "acme-hr"
|
|
1100
|
+
allowedOrchestrators: v.array(v.string()), // ["victor"] or ["*"] for master sentinel
|
|
1101
|
+
scopes: v.array(v.string()), // ["view-own-tasks", "view-own-missions", ...]
|
|
1102
|
+
displayName: v.string(), // "<redacted-client>"
|
|
1103
|
+
isActive: v.boolean(),
|
|
1104
|
+
createdAt: v.number(),
|
|
1105
|
+
})
|
|
1106
|
+
.index("by_clerk_slug", ["clerkOrgSlug"])
|
|
1107
|
+
.index("by_isActive", ["isActive"]),
|
|
1108
|
+
|
|
1109
|
+
// ── userBearerTokens ─────────────────────────────────────────────────────
|
|
1110
|
+
// Bearer tokens issued to VP webapp users via Clerk JWT exchange
|
|
1111
|
+
// (POST /issueBearerFromClerk). Separate from the OAuth DCR flow in
|
|
1112
|
+
// oauth_access_tokens — these are user-grade tokens bound to a Clerk userId
|
|
1113
|
+
// rather than an OAuth client.
|
|
1114
|
+
//
|
|
1115
|
+
// tokenHash = SHA-256 hex of the raw token (raw value NEVER stored).
|
|
1116
|
+
// The raw bearer is returned once in the HTTP response payload and never
|
|
1117
|
+
// re-derivable from the database.
|
|
1118
|
+
//
|
|
1119
|
+
// TTL: 7 days (issuedAt + 604800000 ms).
|
|
1120
|
+
userBearerTokens: defineTable({
|
|
1121
|
+
tokenHash: v.string(), // SHA-256 hex of raw bearer
|
|
1122
|
+
clerkUserId: v.string(), // Clerk `sub` claim
|
|
1123
|
+
workspaceId: v.string(), // logical workspace identifier (clerkUserId-based slug)
|
|
1124
|
+
extId: v.string(), // Chrome extension ID that requested the token
|
|
1125
|
+
expiresAt: v.number(), // ms since epoch
|
|
1126
|
+
revoked: v.boolean(),
|
|
1127
|
+
createdAt: v.number(),
|
|
1128
|
+
lastUsedAt: v.optional(v.number()),
|
|
1129
|
+
})
|
|
1130
|
+
.index("by_token_hash", ["tokenHash"])
|
|
1131
|
+
.index("by_clerkUserId", ["clerkUserId"]),
|
|
1132
|
+
|
|
1133
|
+
// ── credentialsAuditLog ──────────────────────────────────────────────────
|
|
1134
|
+
// Append-only audit trail for every Bearer issuance via issueBearerFromClerk.
|
|
1135
|
+
// Never updated after insert — one row per successful (or failed) issuance.
|
|
1136
|
+
credentialsAuditLog: defineTable({
|
|
1137
|
+
clerkUserId: v.string(),
|
|
1138
|
+
workspaceId: v.string(),
|
|
1139
|
+
extId: v.string(),
|
|
1140
|
+
extVersion: v.optional(v.string()),
|
|
1141
|
+
issuedAt: v.number(),
|
|
1142
|
+
ip: v.optional(v.string()),
|
|
1143
|
+
userAgent: v.optional(v.string()),
|
|
1144
|
+
})
|
|
1145
|
+
.index("by_clerkUserId", ["clerkUserId"])
|
|
1146
|
+
.index("by_workspaceId", ["workspaceId"]),
|
|
1147
|
+
|
|
1148
|
+
// ── credentialsRateLimits ────────────────────────────────────────────────
|
|
1149
|
+
// Rolling rate-limit counter for issueBearerFromClerk.
|
|
1150
|
+
// key = "<clerkUserId>-issueBearer"
|
|
1151
|
+
// Resets automatically when windowStart + 60000 < now.
|
|
1152
|
+
credentialsRateLimits: defineTable({
|
|
1153
|
+
key: v.string(), // "<clerkUserId>-issueBearer"
|
|
1154
|
+
count: v.number(), // requests in current window
|
|
1155
|
+
windowStart: v.number(), // ms since epoch — start of 1-minute window
|
|
1156
|
+
}).index("by_key", ["key"]),
|
|
1157
|
+
|
|
1158
|
+
// ── oauth_audit_log ──────────────────────────────────────────────────────
|
|
1159
|
+
// Append-only audit trail for administrative scope profile mutations.
|
|
1160
|
+
// One row per patchScopeProfileEmergency invocation.
|
|
1161
|
+
// actorTokenHash = sha256Hex of the callerToken (raw token never stored).
|
|
1162
|
+
// previousState + newState allow forensic reconstruction of leaked scopes.
|
|
1163
|
+
// S1.2-mutation: captures Day 90 Nadia `global` leak remediation.
|
|
1164
|
+
// S2.1-D9: clientsRetargeted added (additive, optional for backward compat).
|
|
1165
|
+
oauth_audit_log: defineTable({
|
|
1166
|
+
eventType: v.string(),
|
|
1167
|
+
actorTokenHash: v.string(),
|
|
1168
|
+
targetProfileId: v.string(),
|
|
1169
|
+
previousState: v.object({
|
|
1170
|
+
profileId: v.string(),
|
|
1171
|
+
fromAllowList: v.array(v.string()),
|
|
1172
|
+
namespaceReadPrefixes: v.array(v.string()),
|
|
1173
|
+
namespaceWritePrefixes: v.array(v.string()),
|
|
1174
|
+
}),
|
|
1175
|
+
newState: v.object({
|
|
1176
|
+
profileId: v.string(),
|
|
1177
|
+
fromAllowList: v.array(v.string()),
|
|
1178
|
+
namespaceReadPrefixes: v.array(v.string()),
|
|
1179
|
+
namespaceWritePrefixes: v.array(v.string()),
|
|
1180
|
+
}),
|
|
1181
|
+
reason: v.string(),
|
|
1182
|
+
cascadeRevokedCount: v.number(),
|
|
1183
|
+
// S2.1-D9: number of oauth_clients rows retargeted during rename (0 if no rename).
|
|
1184
|
+
// Optional for backward compat with pre-S2.1 rows.
|
|
1185
|
+
clientsRetargeted: v.optional(v.number()),
|
|
1186
|
+
createdAt: v.number(),
|
|
1187
|
+
})
|
|
1188
|
+
.index("by_targetProfileId", ["targetProfileId"])
|
|
1189
|
+
.index("by_createdAt", ["createdAt"]),
|
|
1190
|
+
|
|
1191
|
+
// ── okfDurableExportProgress ─────────────────────────────────────────────
|
|
1192
|
+
// I1 long-task survival — durable variant of `exportOkfBundle`
|
|
1193
|
+
// (convex/okfBundleDurable.ts). One row per durable job (keyed by the
|
|
1194
|
+
// `@vantageos/agent-engine` `jobId`), tracking pagination cursors across
|
|
1195
|
+
// steps so a step function can resume exactly where the previous one left
|
|
1196
|
+
// off. `orgId` here IS the requested namespace string (see
|
|
1197
|
+
// okfBundleDurable.ts comment) — kept as its own field, never trusted as a
|
|
1198
|
+
// client-supplied scope on any step: every step re-derives its DB reads
|
|
1199
|
+
// from `namespace`/`orgId` on THIS row, written once at job creation by
|
|
1200
|
+
// the authenticated `startOkfBundleExportDurable` mutation.
|
|
1201
|
+
okfDurableExportProgress: defineTable({
|
|
1202
|
+
jobId: v.string(), // opaque id returned by durableJob.start
|
|
1203
|
+
orgId: v.string(), // == namespace; tenant scope for every step read
|
|
1204
|
+
namespace: v.string(),
|
|
1205
|
+
sinceMs: v.optional(v.number()),
|
|
1206
|
+
memoriesCursor: v.union(v.string(), v.null()),
|
|
1207
|
+
memoriesDone: v.boolean(),
|
|
1208
|
+
briefingsCursor: v.union(v.string(), v.null()),
|
|
1209
|
+
briefingsDone: v.boolean(),
|
|
1210
|
+
tasksCursor: v.union(v.string(), v.null()),
|
|
1211
|
+
tasksDone: v.boolean(),
|
|
1212
|
+
memoryCount: v.number(),
|
|
1213
|
+
briefingCount: v.number(),
|
|
1214
|
+
taskCount: v.number(),
|
|
1215
|
+
stepsCompleted: v.number(),
|
|
1216
|
+
status: v.union(v.literal("running"), v.literal("assembled")),
|
|
1217
|
+
createdAt: v.number(),
|
|
1218
|
+
updatedAt: v.number(),
|
|
1219
|
+
})
|
|
1220
|
+
.index("by_jobId", ["jobId"])
|
|
1221
|
+
.index("by_orgId_jobId", ["orgId", "jobId"]),
|
|
1222
|
+
|
|
1223
|
+
// ── okfDurableExportEntries ──────────────────────────────────────────────
|
|
1224
|
+
// Serialized bundle files (path + markdown content) appended one page at a
|
|
1225
|
+
// time by the durable step function. `orgId` is the FIRST index field on
|
|
1226
|
+
// every index (multi-tenant convention) — a step for org A can never read
|
|
1227
|
+
// org B's rows because every query below is scoped by `.eq("orgId", ...)`
|
|
1228
|
+
// before anything else.
|
|
1229
|
+
okfDurableExportEntries: defineTable({
|
|
1230
|
+
orgId: v.string(),
|
|
1231
|
+
jobId: v.string(),
|
|
1232
|
+
family: v.union(
|
|
1233
|
+
v.literal("memory"),
|
|
1234
|
+
v.literal("briefing"),
|
|
1235
|
+
v.literal("task"),
|
|
1236
|
+
),
|
|
1237
|
+
seq: v.number(),
|
|
1238
|
+
path: v.string(),
|
|
1239
|
+
content: v.string(),
|
|
1240
|
+
})
|
|
1241
|
+
.index("by_org_job", ["orgId", "jobId"])
|
|
1242
|
+
.index("by_org_job_seq", ["orgId", "jobId", "seq"]),
|
|
1243
|
+
});
|