diff --git a/packages/harness/src/chain.ts b/packages/harness/src/chain.ts index 7c92713..c393c6f 100644 --- a/packages/harness/src/chain.ts +++ b/packages/harness/src/chain.ts @@ -91,6 +91,14 @@ export const productChain: ArtifactChainConfig = { area: z.string().optional(), }).passthrough(), }, + L: { + prefix: "L", + name: "Learning", + parentTypes: [], + schema: SharedFrontmatter.extend({ + type: z.literal("L"), + }).passthrough(), + }, }, }; diff --git a/packages/harness/test/harness.test.ts b/packages/harness/test/harness.test.ts index bd2f4d3..379c2b6 100644 --- a/packages/harness/test/harness.test.ts +++ b/packages/harness/test/harness.test.ts @@ -78,6 +78,21 @@ it("validates product task frontmatter", () => { expect(report.errors.map((error) => error.code)).toContain("parent_required"); }); +it("accepts product learnings", () => { + const learning = entry({ type: "L", id: "l" }); + const report = validate({ + space: activeSpace({ config: { chain: "product" } }), + chain: productChain, + entries: [learning], + links: [], + entryId: "l", + validateSpecials: false, + }); + + expect(report.ok).toBe(true); + expect(report.errors.map((error) => error.code)).not.toContain("unknown_artifact_type"); +}); + it("scores and ranks WSJF", () => { expect(wsjfScore({ value: 8, time: 5, risk: 2, effort: 4 })).toBe(3.75); expect( diff --git a/services/api/test/api.test.ts b/services/api/test/api.test.ts index c870066..dfc2e21 100644 --- a/services/api/test/api.test.ts +++ b/services/api/test/api.test.ts @@ -20,6 +20,7 @@ import { inboundEvents, insertAuditEvent, integrations, + kbEntries, kbSpaces, llmRequests, migrate, @@ -3433,6 +3434,70 @@ describe("api", async () => { expect(linkedAfter).not.toContain(first.json().id); }); + it("edits existing product learning entries", async () => { + const learningProject = ( + await db + .insert(projects) + .values({ + id: newId("proj"), + orgId, + name: "Product Learning KB", + slug: `product-learning-kb-${Date.now()}`, + settings: {}, + }) + .returning() + )[0]; + expect(learningProject).toBeTruthy(); + const space = await app.inject({ + method: "PUT", + url: `/v1/projects/${learningProject?.id}/kb/space`, + headers: { cookie }, + payload: { config: { chain: "product" } }, + }); + expect(space.statusCode, space.body).toBe(200); + const learningSpace = ( + await db + .select() + .from(kbSpaces) + .where(and(eq(kbSpaces.orgId, orgId), eq(kbSpaces.projectId, learningProject?.id ?? ""))) + .limit(1) + )[0]; + expect(learningSpace).toBeTruthy(); + const learning = ( + await db + .insert(kbEntries) + .values({ + id: newId("kb"), + orgId, + spaceId: learningSpace?.id ?? "", + type: "L", + number: 1, + slug: "agent-learning", + frontmatter: { + id: "L001", + aliases: ["L001"], + type: "L", + created: "2026-07-03", + tags: [], + }, + bodyMd: "Agent-written learning.\n\n## Links\n\n- [[L001]]\n", + }) + .returning() + )[0]; + expect(learning).toBeTruthy(); + + const patched = await app.inject({ + method: "PATCH", + url: `/v1/kb/entries/${learning?.id}`, + headers: { cookie }, + payload: { bodyMd: "Human-edited learning." }, + }); + + expect(patched.statusCode, patched.body).toBe(200); + expect(patched.json().error?.code).not.toBe("unknown_artifact_type"); + expect(patched.json().bodyMd).toContain("Human-edited learning."); + }); + it("returns null for a project whose KB space has not been created", async () => { const legacyProject = ( await db