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:
DottaandClaude Opus 4.8 committed 2026-06-07 03:35:52 +00:00
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
}
]
}
+22
View File
@@ -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",
+27
View File
@@ -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");
});
});
+1
View File
@@ -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();
+3
View File
@@ -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,