refactor(provenance): extract agentSourceFields write-stamp helper (#143 review #5)

The agent write-stamp idiom — `...(isAgent ? { <source>: 'agent', <chat>: aiChatId } : {})`
— was hand-reimplemented at every REST write site, so each new path risked a
wrong literal or a forgotten aiChatId. Extract a single
`agentSourceFields(provenance, sourceKey, chatKey)` next to AuthProvenanceData and
call it at the 5 uniform spread sites:

- comment.service create  -> createdSource / aiChatId
- page.service create/update/orphan-move/move -> lastUpdatedSource / lastUpdatedAiChatId

Sites that must CLEAR the source on a non-agent action keep their own conditional
(comment un-resolve writes an explicit null), and the collab persistence path keeps
its sticky-window logic — both noted in the helper's doc.

Behavior-preserving (the helper returns the identical object/`{}`). Typecheck
clean; server comment/page/auth/collab suites 246 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-24 00:05:54 +03:00
parent 0647faefcd
commit 1d54f8ed1c
3 changed files with 41 additions and 34 deletions

View File

@@ -22,7 +22,10 @@ import {
ICommentResolvedNotificationJob,
} from '../../integrations/queue/constants/queue.interface';
import { WsService } from '../../ws/ws.service';
import { AuthProvenanceData } from '../../common/decorators/auth-provenance.decorator';
import {
AuthProvenanceData,
agentSourceFields,
} from '../../common/decorators/auth-provenance.decorator';
@Injectable()
export class CommentService {
@@ -60,7 +63,6 @@ export class CommentService {
) {
const { page, workspaceId, user } = opts;
const commentContent = JSON.parse(createCommentDto.content);
const isAgent = provenance?.actor === 'agent';
if (createCommentDto.parentCommentId) {
const parentComment = await this.commentRepo.findById(
@@ -87,9 +89,7 @@ export class CommentService {
spaceId: page.spaceId,
// Agent-edit provenance: the user stays creatorId; this only annotates the
// source. Normal user requests leave the column default ('user').
...(isAgent
? { createdSource: 'agent', aiChatId: provenance.aiChatId }
: {}),
...agentSourceFields(provenance, 'createdSource', 'aiChatId'),
});
if (createCommentDto.yjsSelection) {

View File

@@ -57,7 +57,10 @@ import { WatcherService } from '../../watcher/watcher.service';
import { sql } from 'kysely';
import { TransclusionService } from '../transclusion/transclusion.service';
import { remapPageEmbedSourceId } from '../transclusion/utils/transclusion-prosemirror.util';
import { AuthProvenanceData } from '../../../common/decorators/auth-provenance.decorator';
import {
AuthProvenanceData,
agentSourceFields,
} from '../../../common/decorators/auth-provenance.decorator';
@Injectable()
export class PageService {
@@ -135,7 +138,6 @@ export class PageService {
ydoc = createYdocFromJson(prosemirrorJson);
}
const isAgent = provenance?.actor === 'agent';
const page = await this.pageRepo.insertPage({
slugId: generateSlugId(),
@@ -153,12 +155,7 @@ export class PageService {
// Agent-edit provenance. The human stays the responsible author
// (creatorId/lastUpdatedById); these only annotate the source. A normal
// user request leaves the column default ('user').
...(isAgent
? {
lastUpdatedSource: 'agent',
lastUpdatedAiChatId: provenance.aiChatId,
}
: {}),
...agentSourceFields(provenance, 'lastUpdatedSource', 'lastUpdatedAiChatId'),
content,
textContent,
ydoc,
@@ -231,7 +228,6 @@ export class PageService {
contributors.add(user.id);
const contributorIds = Array.from(contributors);
const isAgent = provenance?.actor === 'agent';
// Detect a real title/icon change so the WS tree listener can broadcast an
// `updateOne` to the space (rename / icon swap) WITHOUT re-broadcasting on a
@@ -251,12 +247,7 @@ export class PageService {
lastUpdatedById: user.id,
// Agent-edit provenance: annotate the source without changing the
// responsible author. A normal user request leaves the column default.
...(isAgent
? {
lastUpdatedSource: 'agent',
lastUpdatedAiChatId: provenance.aiChatId,
}
: {}),
...agentSourceFields(provenance, 'lastUpdatedSource', 'lastUpdatedAiChatId'),
updatedAt: new Date(),
contributorIds: contributorIds,
},
@@ -443,7 +434,6 @@ export class PageService {
provenance?: AuthProvenanceData,
) {
let childPageIds: string[] = [];
const isAgent = provenance?.actor === 'agent';
const allPages = await this.pageRepo.getPageAndDescendants(rootPage.id, {
includeContent: false,
@@ -490,12 +480,7 @@ export class PageService {
// Agent-edit provenance on the moved root page. Child pages are bulk
// re-parented to the new space (no content change), so the marker is
// stamped on the root the agent acted on. Normal user: no change.
...(isAgent
? {
lastUpdatedSource: 'agent',
lastUpdatedAiChatId: provenance.aiChatId,
}
: {}),
...agentSourceFields(provenance, 'lastUpdatedSource', 'lastUpdatedAiChatId'),
},
rootPage.id,
trx,
@@ -949,7 +934,6 @@ export class PageService {
}
}
const isAgent = provenance?.actor === 'agent';
const updateResult = await this.pageRepo.updatePage(
{
@@ -957,12 +941,7 @@ export class PageService {
parentPageId: parentPageId,
// Agent-edit provenance: annotate the source on an agent move. A normal
// user request leaves the column default ('user').
...(isAgent
? {
lastUpdatedSource: 'agent',
lastUpdatedAiChatId: provenance.aiChatId,
}
: {}),
...agentSourceFields(provenance, 'lastUpdatedSource', 'lastUpdatedAiChatId'),
},
dto.pageId,
);