FN-9002: model project-owned schema identities
Align project schema declarations with the existing ownership partition. - Model eight project-owned tables with project-leading identities and defaults. - Preserve composite foreign-key and project-scoped uniqueness metadata. - Add PostgreSQL declaration coverage and document deferred runtime predicates. Files changed: ...n-9002-project-schema-ownership-declarations.md | 7 + docs/storage.md | 1 + ...roject-schema-ownership-declarations.pg.test.ts | 245 +++++++++++++++++++++ .../core/src/async-stores/async-secrets-store.ts | 26 +-- packages/core/src/postgres/schema/project.ts | 44 +++- 5 files changed, 298 insertions(+), 25 deletions(-) Fusion-Task-Id: FN-9002 Fusion-Task-Lineage: b6f45d1c-d7d9-4a96-b86e-6d120df3f756 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Model eight project-owned storage tables with their partition identities.
|
||||
category: internal
|
||||
dev: Eight declarations now match the 0006 project partition; runtime predicates land in FN-9000.
|
||||
@@ -38,6 +38,7 @@ See the [2026-07-14 PostgreSQL runtime cutover review](./postgres-migration-revi
|
||||
- `project.agent_ratings` is project-owned with composite `(project_id, id)` identity, allowing the same rating id in separate projects without cross-project reads or deletes. The dynamic `0006_project_ownership.sql` migration reconciles the physical table; `0055_fn_8988_agent_ratings_project_partition.sql` repeats that guarantee idempotently for historical drift. Bound `addRating`, `getRatings`, and `deleteRating` apply the project ownership partition, while unbound compatibility layers retain trigger-stamped writes and unscoped reads/deletes.
|
||||
- FN-8997 audited `workflow_steps`, `chat_room_members`, and `chat_room_messages`: their Drizzle declarations now model 0006's `project_id` and composite keys, and bound workflow/chat helpers scope reads and mutations on that partition. Chat isolation requires both membership/message predicates **and** the parent `chat_rooms.project_id` predicate; either leg alone can resolve a foreign row when room IDs collide. `plugins` remains an intentionally unmodeled compatibility table because it has no runtime Drizzle path. Migration `0056_fn_8997_project_ownership_declaration_drift.sql` is idempotent and adds only partition-prefixed predicate indexes; it does not rewrite healthy 0006 ownership columns or keys.
|
||||
- `project.workflows` has project-local `(project_id, id)` identity. Bound definition reads, updates, deletes, companion workflow settings/prompt-override deletes, and analytics name prefetches use `projectScopeFor`; blank/unbound layers deliberately retain cross-project compatibility reads. The per-project workflow-id counter intentionally scans occupancy across every partition before allocation, because burning a colliding ID is safer than reusing a legacy or stale-counter ID held elsewhere.
|
||||
- FN-9002 makes the `0006_project_ownership.sql` partition expressible for `artifacts`, `secrets`, `branch_groups`, `plugin_activations`, `chat_messages`, `run_audit_events`, `verification_cache`, and `approval_requests`: each declaration retains the database default and models its project-leading identity (plus the artifacts task FK and secrets/branch-name unique keys). Their runtime owners are respectively async attachments/archive/task mutation; async secrets; async branch groups; activation analytics/events; async chat; async audit/merge coordination/data layer; workflow definitions; and async approval requests. This is declaration-only: existing inserts keep omitting `projectId`, public record mappers keep omitting it, and all project predicates are deliberately deferred to FN-9000.
|
||||
- Startup and Batch 1 self-healing expire locks when their lease elapsed or the owner task is terminal/missing. They never move a task or alter scheduler, worktree, semaphore, or verification state. Run-audit events are `symbol-lock:acquired`, `symbol-lock:acquire-conflict`, `symbol-lock:renewed`, `symbol-lock:released`, `symbol-lock:reconcile-stale`, and deduplicated `symbol-lock:reconcile-stale-no-action`; metadata uses only counts/outcomes and normalized opaque keys.
|
||||
- FN-8405 adds `Task.declaredSymbols` as the durable, normalized task declaration source. `## Declared Symbols` in PROMPT.md is parsed only on create/update writes: an absent key may hydrate from the prompt, while a present `undefined`, `null` (update), or `[]` clears and suppresses hydration; a non-empty explicit array wins. Store resolution (`resolveTaskSymbols` and `resolveTaskSymbolsForWorkItem({ taskId })`) reads only the durable field, and slim projections plus archive/restore retain it. Scheduler admission remains a separate FN-8306 consumer; File Scope is never treated as a symbol source.
|
||||
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "vitest";
|
||||
import { getTableConfig } from "drizzle-orm/pg-core";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { AsyncSecretsStore } from "../../async-stores/async-secrets-store.js";
|
||||
import { createApprovalRequest } from "../../async-stores/async-approval-request-store.js";
|
||||
import { addChatMessage } from "../../async-stores/async-chat-store.js";
|
||||
import { recordRunAuditEvent, recordRunAuditEventWithinTransaction } from "../../postgres/data-layer.js";
|
||||
import { insertArtifactRow } from "../../task-store/async/async-comments-attachments.js";
|
||||
import { createBranchGroup } from "../../task-store/async/async-branch-groups.js";
|
||||
import { recordPluginActivation } from "../../task-store/async/async-events.js";
|
||||
import * as schema from "../../postgres/schema/index.js";
|
||||
import {
|
||||
createSharedPgTaskStoreTestHarness,
|
||||
pgDescribe,
|
||||
type SharedPgTaskStoreHarness,
|
||||
} from "../../__test-utils__/pg-test-harness.js";
|
||||
|
||||
const tableNames = [
|
||||
"artifacts",
|
||||
"secrets",
|
||||
"branch_groups",
|
||||
"plugin_activations",
|
||||
"chat_messages",
|
||||
"run_audit_events",
|
||||
"verification_cache",
|
||||
"approval_requests",
|
||||
] as const;
|
||||
|
||||
const declaredTables = {
|
||||
artifacts: schema.project.artifacts,
|
||||
secrets: schema.project.secrets,
|
||||
branch_groups: schema.project.branchGroups,
|
||||
plugin_activations: schema.project.pluginActivations,
|
||||
chat_messages: schema.project.chatMessages,
|
||||
run_audit_events: schema.project.runAuditEvents,
|
||||
verification_cache: schema.project.verificationCache,
|
||||
approval_requests: schema.project.approvalRequests,
|
||||
} as const;
|
||||
|
||||
const expectedPrimaryKeys: Record<(typeof tableNames)[number], string[]> = {
|
||||
artifacts: ["project_id", "id"],
|
||||
secrets: ["project_id", "id"],
|
||||
branch_groups: ["project_id", "id"],
|
||||
plugin_activations: ["project_id", "id"],
|
||||
chat_messages: ["project_id", "id"],
|
||||
run_audit_events: ["project_id", "id"],
|
||||
verification_cache: ["project_id", "tree_sha", "test_command", "build_command"],
|
||||
approval_requests: ["project_id", "id"],
|
||||
};
|
||||
|
||||
pgDescribe("project schema ownership declarations", () => {
|
||||
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||
prefix: "fusion_fn9002_schema",
|
||||
projectId: "fn9002-runtime-bound",
|
||||
});
|
||||
|
||||
beforeAll(h.beforeAll);
|
||||
afterAll(h.afterAll);
|
||||
beforeEach(h.beforeEach);
|
||||
afterEach(h.afterEach);
|
||||
|
||||
it("proves migration 0006 physically partitions all eight drifted tables", async () => {
|
||||
/*
|
||||
FNXC:MultiProjectIsolation 2026-08-12-13:45:
|
||||
Migration 0006 partitioned every project-schema base table before these eight Drizzle
|
||||
declarations caught up. Assert catalog truth first so declaration work cannot guess an
|
||||
ordered key, unique index, or foreign-key name from the pre-isolation snapshot.
|
||||
*/
|
||||
for (const tableName of tableNames) {
|
||||
const columns = await h.adminSql()<Array<{
|
||||
column_name: string;
|
||||
data_type: string;
|
||||
is_nullable: string;
|
||||
column_default: string | null;
|
||||
}>>`
|
||||
SELECT column_name, data_type, is_nullable, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'project' AND table_name = ${tableName} AND column_name = 'project_id'
|
||||
`;
|
||||
expect(columns).toEqual([expect.objectContaining({
|
||||
column_name: "project_id",
|
||||
data_type: "text",
|
||||
is_nullable: "NO",
|
||||
column_default: expect.stringContaining("current_setting"),
|
||||
})]);
|
||||
|
||||
const constraints = await h.adminSql()<Array<{
|
||||
conname: string;
|
||||
contype: string;
|
||||
columns: string[];
|
||||
}>>`
|
||||
SELECT c.conname, c.contype, array_agg(a.attname ORDER BY k.ordinality) AS columns
|
||||
FROM pg_constraint c
|
||||
CROSS JOIN LATERAL unnest(c.conkey) WITH ORDINALITY k(attnum, ordinality)
|
||||
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum
|
||||
WHERE c.conrelid = ('project.' || ${tableName})::regclass
|
||||
AND c.contype IN ('p', 'u', 'f')
|
||||
GROUP BY c.conname, c.contype
|
||||
ORDER BY c.contype, c.conname
|
||||
`;
|
||||
expect(constraints.find((constraint) => constraint.contype === "p")?.columns)
|
||||
.toEqual(expectedPrimaryKeys[tableName]);
|
||||
for (const constraint of constraints) {
|
||||
expect(constraint.columns[0]).toBe("project_id");
|
||||
}
|
||||
|
||||
const uniqueIndexes = await h.adminSql()<Array<{ index_name: string; columns: string[] }>>`
|
||||
SELECT idx.relname AS index_name,
|
||||
array_agg(a.attname ORDER BY key.ordinality) AS columns
|
||||
FROM pg_index i
|
||||
JOIN pg_class idx ON idx.oid = i.indexrelid
|
||||
CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY key(attnum, ordinality)
|
||||
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = key.attnum
|
||||
WHERE i.indrelid = ('project.' || ${tableName})::regclass
|
||||
AND i.indisunique AND NOT i.indisprimary
|
||||
GROUP BY idx.relname
|
||||
`;
|
||||
for (const index of uniqueIndexes) {
|
||||
expect(index.columns[0]).toBe("project_id");
|
||||
}
|
||||
|
||||
const declaration = getTableConfig(declaredTables[tableName]);
|
||||
expect(declaration.columns.map((column) => column.name)).toContain("project_id");
|
||||
expect(declaration.columns.find((column) => column.name === "project_id")?.hasDefault).toBe(true);
|
||||
expect(declaration.primaryKeys).toHaveLength(1);
|
||||
expect(declaration.primaryKeys[0]?.columns.map((column) => column.name))
|
||||
.toEqual(expectedPrimaryKeys[tableName]);
|
||||
}
|
||||
|
||||
const artifactForeignKey = getTableConfig(schema.project.artifacts).foreignKeys
|
||||
.find((key) => key.getName() === "artifacts_task_id_fkey");
|
||||
expect(artifactForeignKey?.reference().columns.map((column) => column.name))
|
||||
.toEqual(["project_id", "task_id"]);
|
||||
expect(artifactForeignKey?.reference().foreignColumns.map((column) => column.name))
|
||||
.toEqual(["project_id", "id"]);
|
||||
expect(getTableConfig(schema.project.secrets).uniqueConstraints.map((key) => ({
|
||||
name: key.getName(), columns: key.columns.map((column) => column.name),
|
||||
}))).toContainEqual({ name: "secrets_key_unique", columns: ["project_id", "key"] });
|
||||
expect(getTableConfig(schema.project.branchGroups).uniqueConstraints.map((key) => ({
|
||||
name: key.getName(), columns: key.columns.map((column) => column.name),
|
||||
}))).toContainEqual({ name: "branch_groups_branch_name_key", columns: ["project_id", "branch_name"] });
|
||||
});
|
||||
|
||||
it("keeps production inserts default-stamped and public mappers partition-free", async () => {
|
||||
/*
|
||||
FNXC:MultiProjectIsolation 2026-08-12-13:45:
|
||||
These are the real async store helpers whose inserts intentionally omit projectId. Exercise
|
||||
them through a GUC-bound layer so the declaration change proves Drizzle still delegates
|
||||
ownership stamping to migration 0006 rather than relying only on catalog or raw-SQL tests.
|
||||
*/
|
||||
const layer = h.layer();
|
||||
const artifact = await insertArtifactRow(layer, {
|
||||
type: "document", title: "Runtime artifact", content: "content", authorId: "agent",
|
||||
authorType: "agent",
|
||||
}, {});
|
||||
const secrets = new AsyncSecretsStore(layer, async () => Buffer.alloc(32, 7));
|
||||
const secret = await secrets.createSecret({ scope: "project", key: "RUNTIME_KEY", plaintextValue: "value" });
|
||||
const branchGroup = await createBranchGroup(layer.db, {
|
||||
sourceType: "planning", sourceId: "runtime", branchName: "runtime-branch",
|
||||
});
|
||||
const activation = await recordPluginActivation(layer.db, { pluginId: "runtime-plugin", source: "test" });
|
||||
const message = await addChatMessage(layer.db, {
|
||||
id: "runtime-message", sessionId: "runtime-session", role: "user", content: "Runtime",
|
||||
thinkingOutput: null, metadata: null, attachments: undefined, createdAt: "2026-08-12T00:00:00.000Z",
|
||||
});
|
||||
const audit = await recordRunAuditEvent(layer, {
|
||||
agentId: "agent", runId: "runtime-run", domain: "test", mutationType: "insert", target: "runtime",
|
||||
});
|
||||
const approval = await createApprovalRequest(layer, {
|
||||
id: "runtime-approval",
|
||||
requester: { actorId: "agent", actorType: "agent", actorName: "Agent" },
|
||||
targetAction: { category: "test", action: "insert", summary: "Runtime", resourceType: "task", resourceId: "runtime" },
|
||||
});
|
||||
await h.store().recordVerificationCachePass("runtime-tree", "test", "build", "runtime-task");
|
||||
await h.store().recordVerificationCachePass("runtime-tree", "test", "build", "updated-task");
|
||||
|
||||
const mapped = [artifact, secret, branchGroup, activation, message, audit, approval];
|
||||
for (const row of mapped) expect("projectId" in row).toBe(false);
|
||||
expect(await h.store().getVerificationCacheHit("runtime-tree", "test", "build"))
|
||||
.toMatchObject({ taskId: "updated-task" });
|
||||
|
||||
for (const tableName of tableNames) {
|
||||
const rows = await h.adminSql()<Array<{ project_id: string }>>`
|
||||
SELECT project_id FROM project.${h.adminSql().unsafe(tableName)}
|
||||
WHERE project_id = 'fn9002-runtime-bound'
|
||||
`;
|
||||
expect(rows.length).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
await layer.transactionImmediate(async (tx) => {
|
||||
await tx.execute(sql`SELECT set_config('fusion.project_id', '', true)`);
|
||||
await recordRunAuditEventWithinTransaction(tx, {
|
||||
agentId: "agent", runId: "unbound-run", domain: "test", mutationType: "insert", target: "unbound",
|
||||
});
|
||||
});
|
||||
expect(await h.adminSql()<Array<{ project_id: string }>>`
|
||||
SELECT project_id FROM project.run_audit_events WHERE run_id = 'unbound-run'
|
||||
`).toEqual([{ project_id: "__legacy_unscoped__" }]);
|
||||
});
|
||||
|
||||
it("keeps duplicate natural identities available in every physical partition", async () => {
|
||||
const insertRows = async (projectId: string): Promise<void> => {
|
||||
await h.adminSql()`SELECT set_config('fusion.project_id', ${projectId}, false)`;
|
||||
await h.adminSql()`INSERT INTO project.artifacts (id, type, title, author_id, created_at, updated_at)
|
||||
VALUES ('shared', 'document', 'Shared', 'agent', '2026-08-12', '2026-08-12')`;
|
||||
await h.adminSql()`INSERT INTO project.secrets (id, key, value_ciphertext, nonce, created_at, updated_at)
|
||||
VALUES ('shared', 'SHARED_KEY', decode('00', 'hex'), decode('00', 'hex'), '2026-08-12', '2026-08-12')`;
|
||||
await h.adminSql()`INSERT INTO project.branch_groups (id, source_type, source_id, branch_name, created_at, updated_at)
|
||||
VALUES ('shared', 'planning', 'shared', 'shared-branch', 1, 1)`;
|
||||
await h.adminSql()`INSERT INTO project.plugin_activations (plugin_id, source, activated_at)
|
||||
VALUES ('shared', 'test', '2026-08-12')`;
|
||||
await h.adminSql()`INSERT INTO project.chat_messages (id, session_id, role, content, created_at)
|
||||
VALUES ('shared', 'shared', 'user', 'Shared', '2026-08-12')`;
|
||||
await h.adminSql()`INSERT INTO project.run_audit_events (id, timestamp, agent_id, run_id, domain, mutation_type, target)
|
||||
VALUES ('shared', '2026-08-12', 'agent', 'run', 'test', 'insert', 'shared')`;
|
||||
await h.adminSql()`INSERT INTO project.verification_cache (tree_sha, test_command, build_command, recorded_at)
|
||||
VALUES ('shared', 'test', 'build', '2026-08-12')`;
|
||||
await h.adminSql()`INSERT INTO project.approval_requests (
|
||||
id, status, requester_actor_id, requester_actor_type, requester_actor_name,
|
||||
target_action_category, target_action_operation, target_action_summary,
|
||||
target_resource_type, target_resource_id, requested_at, created_at, updated_at
|
||||
) VALUES (
|
||||
'shared', 'pending', 'agent', 'agent', 'Agent', 'test', 'insert', 'Shared',
|
||||
'task', 'shared', '2026-08-12', '2026-08-12', '2026-08-12'
|
||||
)`;
|
||||
};
|
||||
|
||||
await insertRows("fn9002-project-a");
|
||||
await insertRows("fn9002-project-b");
|
||||
for (const tableName of tableNames) {
|
||||
const rows = await h.adminSql()<Array<{ count: string }>>`
|
||||
SELECT count(*)::text AS count FROM project.${h.adminSql().unsafe(tableName)}
|
||||
WHERE project_id IN ('fn9002-project-a', 'fn9002-project-b')
|
||||
`;
|
||||
expect(rows).toEqual([{ count: "2" }]);
|
||||
}
|
||||
|
||||
await h.adminSql()`SELECT set_config('fusion.project_id', '', false)`;
|
||||
await h.adminSql()`INSERT INTO project.chat_messages (id, session_id, role, content, created_at)
|
||||
VALUES ('unbound', 'unbound', 'user', 'Unbound', '2026-08-12')`;
|
||||
expect(await h.adminSql()<Array<{ project_id: string }>>`
|
||||
SELECT project_id FROM project.chat_messages WHERE id = 'unbound'
|
||||
`).toEqual([{ project_id: "__legacy_unscoped__" }]);
|
||||
});
|
||||
});
|
||||
@@ -118,24 +118,22 @@ export class SecretsStoreError extends Error {
|
||||
}
|
||||
|
||||
/**
|
||||
* FNXC:SecretsStore 2026-06-24-20:12:
|
||||
* The columns both secrets tables share. project.secrets and
|
||||
* central.secrets_global have identical column shapes but are distinct Drizzle
|
||||
* table objects (different schema/name literal). The helpers operate on the
|
||||
* project.secrets table type and the global table is cast at the dispatch
|
||||
* boundary since the two are structurally identical column-for-column.
|
||||
* FNXC:SecretsStore 2026-08-12-13:45:
|
||||
* Migration 0006 partitions project.secrets with project_id while
|
||||
* central.secrets_global remains global. The shared helpers deliberately cast
|
||||
* the central ref at this dispatch boundary; FN-9000 adds project-only
|
||||
* predicates without leaking a project_id reference into the central leg.
|
||||
*/
|
||||
type ProjectSecretsTable = typeof schema.project.secrets;
|
||||
|
||||
/**
|
||||
* Resolve the Drizzle table ref for a scope. Both tables share the same column
|
||||
* shape, so the call sites are identical once the table ref is selected.
|
||||
* FNXC:SecretsStore 2026-06-24-20:10:
|
||||
* Under the shared PostgreSQL backend a single connection serves both schemas,
|
||||
* so the dual-database injection collapses to a scope-to-table dispatch. The
|
||||
* central.secrets_global table is structurally identical to project.secrets
|
||||
* (same columns, same types), so it is cast to the project table type at the
|
||||
* dispatch boundary; the helper bodies then compile against one table type.
|
||||
* Resolve the Drizzle table ref for a scope. The deliberate shape bridge keeps
|
||||
* current shared helper bodies stable until project-only scoping arrives.
|
||||
* FNXC:SecretsStore 2026-08-12-13:45:
|
||||
* A shared PostgreSQL connection serves both schemas. project.secrets now
|
||||
* models its 0006 partition while central.secrets_global does not have that
|
||||
* column, so the cast is a dispatch-only compatibility bridge, not evidence
|
||||
* that their physical shapes are identical.
|
||||
*/
|
||||
function tableForScope(scope: SecretScope): ProjectSecretsTable {
|
||||
return scope === "project"
|
||||
|
||||
@@ -1171,8 +1171,15 @@ export const taskDocuments = projectSchema.table("task_documents", {
|
||||
index("idxTaskDocumentsTaskId").on(t.taskId),
|
||||
]);
|
||||
|
||||
/*
|
||||
FNXC:MultiProjectIsolation 2026-08-12-13:45:
|
||||
Migration 0006 physically partitions these declarations, but the Drizzle schema drifted and could
|
||||
not express their project_id-leading identities. Keep the database current_setting default so
|
||||
bound and unbound trigger-stamped inserts omit projectId; FN-9000 owns runtime predicates.
|
||||
*/
|
||||
export const artifacts = projectSchema.table("artifacts", {
|
||||
id: text("id").primaryKey(),
|
||||
projectId: text("project_id").notNull().default(sql`current_setting('fusion.project_id', true)`),
|
||||
id: text("id").notNull(),
|
||||
type: text("type").notNull(),
|
||||
title: text("title").notNull(),
|
||||
description: text("description"),
|
||||
@@ -1187,7 +1194,8 @@ export const artifacts = projectSchema.table("artifacts", {
|
||||
createdAt: text("created_at").notNull(),
|
||||
updatedAt: text("updated_at").notNull(),
|
||||
}, (t) => [
|
||||
foreignKey({ columns: [t.taskId], foreignColumns: [tasks.id] }).onDelete("cascade"),
|
||||
primaryKey({ columns: [t.projectId, t.id] }),
|
||||
foreignKey({ name: "artifacts_task_id_fkey", columns: [t.projectId, t.taskId], foreignColumns: [tasks.projectId, tasks.id] }).onDelete("cascade"),
|
||||
index("idxArtifactsTaskId").on(t.taskId),
|
||||
index("idxArtifactsAuthorId").on(t.authorId),
|
||||
index("idxArtifactsType").on(t.type),
|
||||
@@ -1391,7 +1399,8 @@ export const evalRunEvents = projectSchema.table("eval_run_events", {
|
||||
|
||||
// ── Secrets (project-scoped) ─────────────────────────────────────────
|
||||
export const secrets = projectSchema.table("secrets", {
|
||||
id: text("id").primaryKey(),
|
||||
projectId: text("project_id").notNull().default(sql`current_setting('fusion.project_id', true)`),
|
||||
id: text("id").notNull(),
|
||||
key: text("key").notNull(),
|
||||
valueCiphertext: bytea("value_ciphertext").notNull(),
|
||||
nonce: bytea("nonce").notNull(),
|
||||
@@ -1404,7 +1413,8 @@ export const secrets = projectSchema.table("secrets", {
|
||||
lastReadAt: text("last_read_at"),
|
||||
lastReadBy: text("last_read_by"),
|
||||
}, (t) => [
|
||||
unique("secrets_key_unique").on(t.key),
|
||||
primaryKey({ columns: [t.projectId, t.id] }),
|
||||
unique("secrets_key_unique").on(t.projectId, t.key),
|
||||
check("secrets_access_policy_check", sql`${t.accessPolicy} IN ('auto', 'prompt', 'deny')`),
|
||||
check("secrets_env_exportable_check", sql`${t.envExportable} IN (0, 1)`),
|
||||
]);
|
||||
@@ -1451,10 +1461,11 @@ export const missions = projectSchema.table("missions", {
|
||||
}, (t) => [primaryKey({ columns: [t.projectId, t.id] })]);
|
||||
|
||||
export const branchGroups = projectSchema.table("branch_groups", {
|
||||
id: text("id").primaryKey(),
|
||||
projectId: text("project_id").notNull().default(sql`current_setting('fusion.project_id', true)`),
|
||||
id: text("id").notNull(),
|
||||
sourceType: text("source_type").notNull(),
|
||||
sourceId: text("source_id").notNull(),
|
||||
branchName: text("branch_name").notNull().unique(),
|
||||
branchName: text("branch_name").notNull(),
|
||||
worktreePath: text("worktree_path"),
|
||||
autoMerge: integer("auto_merge").notNull().default(0),
|
||||
prState: text("pr_state").notNull().default("none"),
|
||||
@@ -1470,6 +1481,8 @@ export const branchGroups = projectSchema.table("branch_groups", {
|
||||
updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
|
||||
closedAt: bigint("closed_at", { mode: "number" }),
|
||||
}, (t) => [
|
||||
primaryKey({ columns: [t.projectId, t.id] }),
|
||||
unique("branch_groups_branch_name_key").on(t.projectId, t.branchName),
|
||||
check("branch_groups_source_type_check", sql`${t.sourceType} IN ('mission','planning','new-task')`),
|
||||
check("branch_groups_pr_state_check", sql`${t.prState} IN ('none','open','merged','closed')`),
|
||||
check("branch_groups_status_check", sql`${t.status} IN ('open','finalized','abandoned')`),
|
||||
@@ -1905,12 +1918,14 @@ export const usageEvents = projectSchema.table("usage_events", {
|
||||
]);
|
||||
|
||||
export const pluginActivations = projectSchema.table("plugin_activations", {
|
||||
id: integer("id").generatedAlwaysAsIdentity().primaryKey(),
|
||||
projectId: text("project_id").notNull().default(sql`current_setting('fusion.project_id', true)`),
|
||||
id: integer("id").generatedAlwaysAsIdentity().notNull(),
|
||||
pluginId: text("plugin_id").notNull(),
|
||||
source: text("source").notNull(),
|
||||
pluginVersion: text("plugin_version"),
|
||||
activatedAt: text("activated_at").notNull(),
|
||||
}, (t) => [
|
||||
primaryKey({ columns: [t.projectId, t.id] }),
|
||||
index("idxPluginActivationsActivatedAt").on(t.activatedAt),
|
||||
index("idxPluginActivationsPluginId").on(t.pluginId),
|
||||
]);
|
||||
@@ -2178,7 +2193,8 @@ export const cliSessions = projectSchema.table("cli_sessions", {
|
||||
]);
|
||||
|
||||
export const chatMessages = projectSchema.table("chat_messages", {
|
||||
id: text("id").primaryKey(),
|
||||
projectId: text("project_id").notNull().default(sql`current_setting('fusion.project_id', true)`),
|
||||
id: text("id").notNull(),
|
||||
sessionId: text("session_id").notNull(),
|
||||
role: text("role").notNull(),
|
||||
content: text("content").notNull(),
|
||||
@@ -2187,6 +2203,7 @@ export const chatMessages = projectSchema.table("chat_messages", {
|
||||
createdAt: text("created_at").notNull(),
|
||||
attachments: jsonb("attachments"),
|
||||
}, (t) => [
|
||||
primaryKey({ columns: [t.projectId, t.id] }),
|
||||
index("idxChatMessagesSessionId").on(t.sessionId),
|
||||
index("idxChatMessagesCreatedAt").on(t.createdAt),
|
||||
]);
|
||||
@@ -2218,7 +2235,8 @@ export const chatTokenUsage = projectSchema.table("chat_token_usage", {
|
||||
]);
|
||||
|
||||
export const runAuditEvents = projectSchema.table("run_audit_events", {
|
||||
id: text("id").primaryKey(),
|
||||
projectId: text("project_id").notNull().default(sql`current_setting('fusion.project_id', true)`),
|
||||
id: text("id").notNull(),
|
||||
timestamp: text("timestamp").notNull(),
|
||||
taskId: text("task_id"),
|
||||
agentId: text("agent_id").notNull(),
|
||||
@@ -2228,6 +2246,7 @@ export const runAuditEvents = projectSchema.table("run_audit_events", {
|
||||
target: text("target").notNull(),
|
||||
metadata: jsonb("metadata"),
|
||||
}, (t) => [
|
||||
primaryKey({ columns: [t.projectId, t.id] }),
|
||||
index("idxRunAuditEventsRunIdTimestamp").on(t.runId, t.timestamp),
|
||||
index("idxRunAuditEventsTaskIdTimestamp").on(t.taskId, t.timestamp),
|
||||
index("idxRunAuditEventsTimestamp").on(t.timestamp),
|
||||
@@ -2340,13 +2359,14 @@ export const missionLineageStops = projectSchema.table("mission_lineage_stops",
|
||||
]);
|
||||
|
||||
export const verificationCache = projectSchema.table("verification_cache", {
|
||||
projectId: text("project_id").notNull().default(sql`current_setting('fusion.project_id', true)`),
|
||||
treeSha: text("tree_sha").notNull(),
|
||||
testCommand: text("test_command").notNull().default(""),
|
||||
buildCommand: text("build_command").notNull().default(""),
|
||||
recordedAt: text("recorded_at").notNull(),
|
||||
taskId: text("task_id"),
|
||||
}, (t) => [
|
||||
primaryKey({ columns: [t.treeSha, t.testCommand, t.buildCommand] }),
|
||||
primaryKey({ columns: [t.projectId, t.treeSha, t.testCommand, t.buildCommand] }),
|
||||
index("idxVerificationCacheRecordedAt").on(t.recordedAt),
|
||||
]);
|
||||
|
||||
@@ -2387,7 +2407,8 @@ export const importTranslationCache = projectSchema.table("import_translation_ca
|
||||
]);
|
||||
|
||||
export const approvalRequests = projectSchema.table("approval_requests", {
|
||||
id: text("id").primaryKey(),
|
||||
projectId: text("project_id").notNull().default(sql`current_setting('fusion.project_id', true)`),
|
||||
id: text("id").notNull(),
|
||||
status: text("status").notNull(),
|
||||
requesterActorId: text("requester_actor_id").notNull(),
|
||||
requesterActorType: text("requester_actor_type").notNull(),
|
||||
@@ -2406,6 +2427,7 @@ export const approvalRequests = projectSchema.table("approval_requests", {
|
||||
createdAt: text("created_at").notNull(),
|
||||
updatedAt: text("updated_at").notNull(),
|
||||
}, (t) => [
|
||||
primaryKey({ columns: [t.projectId, t.id] }),
|
||||
index("idxApprovalRequestsStatusCreatedAt").on(t.status, t.createdAt),
|
||||
index("idxApprovalRequestsRequesterCreatedAt").on(t.requesterActorId, t.createdAt),
|
||||
index("idxApprovalRequestsTaskCreatedAt").on(t.taskId, t.createdAt),
|
||||
|
||||
Reference in New Issue
Block a user