Q33: document edits and content deletions leave attributed audit records

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 06:45:51 -04:00
1 parent cb464c3ee4
commit 66de4ef015
9 files changed
+147 -16

No files matched your search

+8
View File
@@ -59,6 +59,14 @@ export class AuditService {
);
}
/** Content changes name a real member of the organization as the actor. */
requireActor(orgId: string, actorType: "employee" | "agent", actorId: string): void {
const table = { employee: "employees", agent: "agents" }[actorType];
if (!table) throw new Error(`Unknown actor type "${actorType}".`);
const actor = this.db.prepare(`select id from ${table} where id = ? and org_id = ?`).get(actorId, orgId);
if (!actor) throw new Error(`No ${actorType} "${actorId}" in this organization.`);
}
createThreadReplay(input: CreateAuditReplayInput): string {
const thread = this.requireThread(input.orgId, input.threadId);
this.requireEmployee(input.orgId, input.createdByEmployeeId);
+26 -4
View File
@@ -48,8 +48,9 @@ export class DocumentService {
update(input: UpdateDocumentInput): number {
const reason = input.reason.trim();
if (!reason) throw new Error("A document update needs a reason.");
const current = this.db.prepare("select version from documents where id = ? and org_id = ?")
.get(input.documentId, input.orgId) as { version: number } | undefined;
this.audit.requireActor(input.orgId, input.changedByType, input.changedById);
const current = this.db.prepare("select version, title, status from documents where id = ? and org_id = ?")
.get(input.documentId, input.orgId) as { version: number; title: string; status: string } | undefined;
if (!current) throw new Error("Document was not found.");
const version = current.version + 1;
const now = new Date().toISOString();
@@ -60,6 +61,15 @@ export class DocumentService {
orgId: input.orgId, documentId: input.documentId, version, title: input.title.trim(), content: input.content, status: input.status,
changedByType: input.changedByType, changedById: input.changedById, reason, at: now,
});
this.audit.record({
orgId: input.orgId,
entityType: "document",
entityId: input.documentId,
actorType: input.changedByType,
actorId: input.changedById,
action: "revised",
payload: { version, reason, title: input.title.trim(), status: input.status, previousTitle: current.title, previousStatus: current.status },
});
return version;
}
@@ -71,8 +81,20 @@ export class DocumentService {
}
delete(input: DeleteDocumentInput): void {
const result = this.db.prepare("delete from documents where id = ? and org_id = ?").run(input.documentId, input.orgId);
if (result.changes === 0) throw new Error("Document was not found.");
this.audit.requireActor(input.orgId, "employee", input.deletedByEmployeeId);
const doc = this.db.prepare("select title, doc_type, status, version, content from documents where id = ? and org_id = ?")
.get(input.documentId, input.orgId) as { title: string; doc_type: string; status: string; version: number; content: string } | undefined;
if (!doc) throw new Error("Document was not found.");
this.db.prepare("delete from documents where id = ? and org_id = ?").run(input.documentId, input.orgId);
this.audit.record({
orgId: input.orgId,
entityType: "document",
entityId: input.documentId,
actorType: "employee",
actorId: input.deletedByEmployeeId,
action: "deleted",
payload: { title: doc.title, docType: doc.doc_type, status: doc.status, version: doc.version, content: doc.content },
});
}
private writeVersion(input: {
+24 -3
View File
@@ -87,6 +87,7 @@ export class KnowledgeService {
update(input: UpdateKnowledgeEntryInput): number {
const reason = input.reason.trim();
if (!reason) throw new Error("A knowledge correction needs a reason.");
this.audit.requireActor(input.orgId, input.changedByType, input.changedById);
const now = new Date().toISOString();
const result = this.db.prepare(`
update knowledge_entries set topic = ?, category = ?, content = ?, importance = ?, updated_at = ?
@@ -104,7 +105,7 @@ export class KnowledgeService {
orgId: input.orgId,
entityType: "knowledge_entry",
entityId: input.entryId,
actorType: input.changedByType === "agent" ? "agent" : "employee",
actorType: input.changedByType,
actorId: input.changedById,
action: "revised",
payload: { revision, reason },
@@ -120,8 +121,28 @@ export class KnowledgeService {
}
delete(input: DeleteKnowledgeEntryInput): void {
const result = this.db.prepare("delete from knowledge_entries where id = ? and org_id = ?").run(input.entryId, input.orgId);
if (result.changes === 0) throw new Error("Knowledge entry was not found.");
this.audit.requireActor(input.orgId, "employee", input.deletedByEmployeeId);
const entry = this.db.prepare(`
select topic, category, content, importance, scope_type, scope_id,
(select max(revision) from knowledge_revisions where entry_id = knowledge_entries.id) as revision
from knowledge_entries where id = ? and org_id = ?
`).get(input.entryId, input.orgId) as {
topic: string; category: string; content: string; importance: string; scope_type: string; scope_id: string | null; revision: number;
} | undefined;
if (!entry) throw new Error("Knowledge entry was not found.");
this.db.prepare("delete from knowledge_entries where id = ? and org_id = ?").run(input.entryId, input.orgId);
this.audit.record({
orgId: input.orgId,
entityType: "knowledge_entry",
entityId: input.entryId,
actorType: "employee",
actorId: input.deletedByEmployeeId,
action: "deleted",
payload: {
topic: entry.topic, category: entry.category, content: entry.content, importance: entry.importance,
scopeType: entry.scope_type, scopeId: entry.scope_id, revision: entry.revision,
},
});
}
private writeRevision(input: {
@@ -140,7 +140,7 @@ export function DocumentsPanel({
onClose={() => setConfirmDelete(false)}
onSubmit={(event) => {
event.preventDefault();
void perform(() => actions.run(() => window.agentCompany.deleteDocument({ orgId, documentId: selected.id })), () => {
void perform(() => actions.run(() => window.agentCompany.deleteDocument({ orgId, documentId: selected.id, deletedByEmployeeId: employeeId })), () => {
setConfirmDelete(false);
setSelectedId(null);
});
@@ -202,7 +202,7 @@ export function KnowledgePanel({
onClose={() => setDialog(null)}
onSubmit={(event) => {
event.preventDefault();
perform(() => actions.run(() => window.agentCompany.deleteKnowledgeEntry({ orgId, entryId: selected.id })), () => {
perform(() => actions.run(() => window.agentCompany.deleteKnowledgeEntry({ orgId, entryId: selected.id, deletedByEmployeeId: employeeId })), () => {
setDialog(null);
setSelectedId(null);
});
+5 -3
View File
@@ -19,12 +19,13 @@ export interface UpdateDocumentInput {
/** Why it changed; kept with the new version. */
reason: string;
changedByType: "employee" | "agent";
changedById: string | null;
changedById: string;
}
export interface DeleteDocumentInput {
orgId: string;
documentId: string;
deletedByEmployeeId: string;
}
export type KnowledgeScopeType = "organization" | "project" | "goal" | "task" | "agent";
@@ -55,8 +56,8 @@ export interface UpdateKnowledgeEntryInput {
importance: string;
/** Why it changed; kept in the revision history. */
reason: string;
changedByType: string;
changedById: string | null;
changedByType: "employee" | "agent";
changedById: string;
}
export interface DocumentVersionRecord {
@@ -95,6 +96,7 @@ export interface ListKnowledgeRevisionsInput {
export interface DeleteKnowledgeEntryInput {
orgId: string;
entryId: string;
deletedByEmployeeId: string;
}
export interface CreateAutomationRuleInput {