mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
fix(documents): say whether an exact revision read is still current
An agent asked to read the current plan fetched the revision it had just restored by id and reported it as the latest, missing a newer edit by another person. The exact-revision read now carries the document's latestRevisionId, latestRevisionNumber and isLatest, and its API description points to the current-document read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dc6e4be04d
commit
e804362a94
7 files changed
+18
-3
No files matched your search
@@ -1220,6 +1220,7 @@ export type {
|
||||
IssueDocument,
|
||||
IssueDocumentSummary,
|
||||
DocumentRevision,
|
||||
ExactDocumentRevision,
|
||||
DocumentFormat,
|
||||
LegacyPlanDocument,
|
||||
IssueAttachment,
|
||||
|
||||
@@ -820,6 +820,7 @@ export type {
|
||||
IssueDocument,
|
||||
IssueDocumentSummary,
|
||||
DocumentRevision,
|
||||
ExactDocumentRevision,
|
||||
DocumentFormat,
|
||||
LegacyPlanDocument,
|
||||
IssueAncestor,
|
||||
|
||||
@@ -214,6 +214,13 @@ export interface DocumentRevision {
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
/** One exact revision plus the document's current revision, so a pinned read never passes as the latest. */
|
||||
export interface ExactDocumentRevision extends DocumentRevision {
|
||||
latestRevisionId: string | null;
|
||||
latestRevisionNumber: number;
|
||||
isLatest: boolean;
|
||||
}
|
||||
|
||||
export interface LegacyPlanDocument {
|
||||
key: "plan";
|
||||
body: string;
|
||||
|
||||
@@ -109,6 +109,9 @@ describeEmbeddedPostgres("documentService system issue documents", () => {
|
||||
const pinned = await svc.getIssueDocumentRevision(issueId, "plan", original!.latestRevisionId!);
|
||||
expect(pinned?.body).toBe("# Plan");
|
||||
expect(pinned?.id).toBe(original!.latestRevisionId);
|
||||
const current = await svc.getIssueDocumentByKey(issueId, "plan");
|
||||
expect(pinned).toMatchObject({ isLatest: false, latestRevisionId: current!.latestRevisionId, latestRevisionNumber: 2 });
|
||||
expect(await svc.getIssueDocumentRevision(issueId, "plan", current!.latestRevisionId!)).toMatchObject({ isLatest: true, revisionNumber: 2, body: "# Newer plan" });
|
||||
expect(await svc.getIssueDocumentRevision(issueId, "other-document", original!.latestRevisionId!)).toBeNull();
|
||||
expect(await svc.getIssueDocumentRevision(randomUUID(), "plan", original!.latestRevisionId!)).toBeNull();
|
||||
expect(await svc.getIssueDocumentRevision(issueId, "plan", randomUUID())).toBeNull();
|
||||
|
||||
@@ -4624,7 +4624,7 @@ registry.registerPath({
|
||||
tags: ["issues"],
|
||||
summary: "Get an exact issue document revision",
|
||||
description:
|
||||
"Available to board users and agents with current issue read access. Returns the specified revision only when its document is linked to this issue under the given key. Document keys are trimmed and lowercased before validation. This read does not restore or modify the document.",
|
||||
"Available to board users and agents with current issue read access. Returns the specified revision only when its document is linked to this issue under the given key. Document keys are trimmed and lowercased before validation. The response also carries the document's current latestRevisionId and latestRevisionNumber, and isLatest says whether this revision is still current; to read the current text, use GET /api/issues/{id}/documents/{key}. This read does not restore or modify the document.",
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string(),
|
||||
|
||||
@@ -181,11 +181,13 @@ export function documentService(db: Db) {
|
||||
format: documentRevisions.format, body: documentRevisions.body, changeSummary: documentRevisions.changeSummary,
|
||||
createdByAgentId: documentRevisions.createdByAgentId, createdByUserId: documentRevisions.createdByUserId,
|
||||
createdByRunId: documentRevisions.createdByRunId, createdAt: documentRevisions.createdAt,
|
||||
latestRevisionId: documents.latestRevisionId, latestRevisionNumber: documents.latestRevisionNumber,
|
||||
}).from(issueDocuments)
|
||||
.innerJoin(documents, eq(issueDocuments.documentId, documents.id))
|
||||
.innerJoin(documentRevisions, and(eq(documentRevisions.documentId, documents.id), eq(documentRevisions.companyId, documents.companyId)))
|
||||
.where(and(eq(issueDocuments.issueId, issueId), eq(issueDocuments.key, key), eq(documentRevisions.id, revisionId))).limit(1);
|
||||
return revision ?? null;
|
||||
// An exact read also states whether it is still current, so a reader cannot report an older revision as the latest one.
|
||||
return revision ? { ...revision, isLatest: revision.latestRevisionId === revision.id } : null;
|
||||
},
|
||||
|
||||
listIssueDocumentRevisions: async (issueId: string, rawKey: string) => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
InboxMineSnapshot,
|
||||
CreateIssueTreeHold,
|
||||
DocumentRevision,
|
||||
ExactDocumentRevision,
|
||||
FeedbackTargetType,
|
||||
FeedbackTrace,
|
||||
FeedbackVote,
|
||||
@@ -745,7 +746,7 @@ export const issuesApi = {
|
||||
`/issues/${id}/documents/${encodeURIComponent(key)}/revisions`,
|
||||
),
|
||||
getDocumentRevision: (id: string, key: string, revisionId: string) =>
|
||||
api.get<DocumentRevision>(`/issues/${id}/documents/${encodeURIComponent(key)}/revisions/${encodeURIComponent(revisionId)}`),
|
||||
api.get<ExactDocumentRevision>(`/issues/${id}/documents/${encodeURIComponent(key)}/revisions/${encodeURIComponent(revisionId)}`),
|
||||
restoreDocumentRevision: (id: string, key: string, revisionId: string) =>
|
||||
api.post<IssueDocument>(
|
||||
`/issues/${id}/documents/${encodeURIComponent(key)}/revisions/${revisionId}/restore`,
|
||||
|
||||
Reference in new issue
Block a user