mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
Derive document_type from issue document key so Type filter matches (PAP-10581)
Auto-generated issue docs were always created with documentType "other", so the Documents library Type filter (Plan/Spec/Brief/Report) never matched the plan/spec/brief/report key chip shown in each row. Map the issue document key to its categorical documentType on create (stripping the locked-document `-N` fallback suffix), add an idempotent backfill that only touches rows still at the default "other", and cover both with tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
931e7ac40d
commit
97ec26b11d
7 files changed
+114
No files matched your search
@@ -0,0 +1,19 @@
|
||||
-- Backfill documents.document_type from their issue-document key so the
|
||||
-- Documents library "Type" filter matches the key chip rendered in each row.
|
||||
-- Auto-generated issue docs were created with document_type = 'other'
|
||||
-- regardless of key (plan/spec/brief/report), so Type=Plan never matched.
|
||||
-- Idempotent: only rows still at the default 'other' are touched, preserving
|
||||
-- any document_type a user set manually. Locked-document fallback keys carry a
|
||||
-- numeric suffix (e.g. plan-2), which is stripped before matching.
|
||||
UPDATE "documents" AS d
|
||||
SET "document_type" = CASE regexp_replace(lower(btrim(id."key")), '-[0-9]+$', '')
|
||||
WHEN 'plan' THEN 'plan'
|
||||
WHEN 'spec' THEN 'spec'
|
||||
WHEN 'brief' THEN 'brief'
|
||||
WHEN 'report' THEN 'report'
|
||||
ELSE d."document_type"
|
||||
END
|
||||
FROM "issue_documents" AS id
|
||||
WHERE id."document_id" = d."id"
|
||||
AND d."document_type" = 'other'
|
||||
AND regexp_replace(lower(btrim(id."key")), '-[0-9]+$', '') IN ('plan', 'spec', 'brief', 'report');
|
||||
@@ -715,6 +715,13 @@
|
||||
"when": 1780794600000,
|
||||
"tag": "0101_document_suggestion_resolved_status",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 102,
|
||||
"version": "7",
|
||||
"when": 1780794900000,
|
||||
"tag": "0102_backfill_document_type_from_issue_key",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -371,6 +371,28 @@ export type DocumentStatus = (typeof DOCUMENT_STATUSES)[number];
|
||||
export const DOCUMENT_TYPES = ["plan", "spec", "brief", "report", "other"] as const;
|
||||
export type DocumentType = (typeof DOCUMENT_TYPES)[number];
|
||||
|
||||
/**
|
||||
* Categorical {@link DocumentType} values an issue-document key can map onto.
|
||||
* Excludes the catch-all `other`, which is the fallback for any unmatched key.
|
||||
*/
|
||||
const ISSUE_DOCUMENT_KEY_TYPE_MAP: Record<string, DocumentType> = {
|
||||
plan: "plan",
|
||||
spec: "spec",
|
||||
brief: "brief",
|
||||
report: "report",
|
||||
};
|
||||
|
||||
/**
|
||||
* Derive the categorical {@link DocumentType} for an auto-generated issue
|
||||
* document from its key (e.g. `plan` → `plan`). Locked-document fallback keys
|
||||
* carry a numeric suffix (`plan-2`), so that is stripped before matching.
|
||||
* Anything that doesn't match a known category falls back to `other`.
|
||||
*/
|
||||
export function documentTypeForIssueDocumentKey(key: string): DocumentType {
|
||||
const base = key.trim().toLowerCase().replace(/-[0-9]+$/, "");
|
||||
return ISSUE_DOCUMENT_KEY_TYPE_MAP[base] ?? "other";
|
||||
}
|
||||
|
||||
export const DOCUMENT_LINK_TARGET_TYPES = [
|
||||
"issue",
|
||||
"project",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { documentTypeForIssueDocumentKey } from "./constants.js";
|
||||
|
||||
describe("documentTypeForIssueDocumentKey", () => {
|
||||
it("maps known categorical keys to their document type", () => {
|
||||
expect(documentTypeForIssueDocumentKey("plan")).toBe("plan");
|
||||
expect(documentTypeForIssueDocumentKey("spec")).toBe("spec");
|
||||
expect(documentTypeForIssueDocumentKey("brief")).toBe("brief");
|
||||
expect(documentTypeForIssueDocumentKey("report")).toBe("report");
|
||||
});
|
||||
|
||||
it("strips the locked-document numeric suffix before matching", () => {
|
||||
expect(documentTypeForIssueDocumentKey("plan-2")).toBe("plan");
|
||||
expect(documentTypeForIssueDocumentKey("report-17")).toBe("report");
|
||||
});
|
||||
|
||||
it("normalizes case and whitespace", () => {
|
||||
expect(documentTypeForIssueDocumentKey(" Plan ")).toBe("plan");
|
||||
});
|
||||
|
||||
it("falls back to other for unknown or system keys", () => {
|
||||
expect(documentTypeForIssueDocumentKey("continuation-summary")).toBe("other");
|
||||
expect(documentTypeForIssueDocumentKey("browsable-files")).toBe("other");
|
||||
expect(documentTypeForIssueDocumentKey("planning")).toBe("other");
|
||||
expect(documentTypeForIssueDocumentKey("plan-notes")).toBe("other");
|
||||
});
|
||||
});
|
||||
@@ -96,6 +96,7 @@ export {
|
||||
DOCUMENT_SUGGESTION_STATUSES,
|
||||
DOCUMENT_STATUSES,
|
||||
DOCUMENT_TYPES,
|
||||
documentTypeForIssueDocumentKey,
|
||||
DOCUMENT_LINK_TARGET_TYPES,
|
||||
ISSUE_EXECUTION_POLICY_MODES,
|
||||
ISSUE_EXECUTION_STAGE_TYPES,
|
||||
|
||||
@@ -196,6 +196,41 @@ describeEmbeddedPostgres("documentService system issue documents", () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it("derives documentType from the issue document key so the Type filter matches", async () => {
|
||||
const { issueId } = await createIssueWithDocuments();
|
||||
|
||||
const plan = await svc.getIssueDocumentByKey(issueId, "plan");
|
||||
const summary = await svc.getIssueDocumentByKey(issueId, ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY);
|
||||
expect(plan?.documentType).toBe("plan");
|
||||
// Unknown / system keys stay categorical "other".
|
||||
expect(summary?.documentType).toBe("other");
|
||||
|
||||
// Locked-document fallback keys (plan-2) inherit the same type.
|
||||
const locked = await svc.lockIssueDocument({ issueId, key: "plan", lockedByUserId: "board-user" });
|
||||
const fallback = await svc.upsertIssueDocument({
|
||||
issueId,
|
||||
key: "plan",
|
||||
title: "Plan",
|
||||
format: "markdown",
|
||||
body: "# Replacement plan",
|
||||
baseRevisionId: locked.document.latestRevisionId,
|
||||
lockedDocumentStrategy: "create_new_document",
|
||||
});
|
||||
expect(fallback.document.key).toBe("plan-2");
|
||||
expect(fallback.document.documentType).toBe("plan");
|
||||
|
||||
// The company library Type filter (documentType) now matches plan docs.
|
||||
const planDocs = await svc.listCompanyDocuments(plan!.companyId, {
|
||||
type: ["plan"],
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
});
|
||||
const planIds = planDocs.map((doc) => doc.id);
|
||||
expect(planIds).toContain(plan!.id);
|
||||
expect(planIds).toContain(fallback.document.id);
|
||||
expect(planIds).not.toContain(summary!.id);
|
||||
});
|
||||
|
||||
it("maintains document links for issue-document compatibility and company library queries", async () => {
|
||||
const { issueId } = await createIssueWithDocuments();
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
type DocumentLinkTargetType,
|
||||
type DocumentStatus,
|
||||
type DocumentType,
|
||||
documentTypeForIssueDocumentKey,
|
||||
isSystemIssueDocumentKey,
|
||||
issueDocumentKeySchema,
|
||||
} from "@paperclipai/shared";
|
||||
@@ -863,6 +864,7 @@ export function documentService(db: Db) {
|
||||
companyId: issue.companyId,
|
||||
title: input.title ?? null,
|
||||
format: input.format,
|
||||
documentType: documentTypeForIssueDocumentKey(fallbackKey),
|
||||
latestBody: input.body,
|
||||
latestRevisionId: null,
|
||||
latestRevisionNumber: 1,
|
||||
@@ -1047,6 +1049,7 @@ export function documentService(db: Db) {
|
||||
companyId: issue.companyId,
|
||||
title: input.title ?? null,
|
||||
format: input.format,
|
||||
documentType: documentTypeForIssueDocumentKey(key),
|
||||
latestBody: input.body,
|
||||
latestRevisionId: null,
|
||||
latestRevisionNumber: 1,
|
||||
|
||||
Reference in new issue
Block a user