Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/harness/src/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
},
},
};

Expand Down
15 changes: 15 additions & 0 deletions packages/harness/test/harness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
65 changes: 65 additions & 0 deletions services/api/test/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
inboundEvents,
insertAuditEvent,
integrations,
kbEntries,
kbSpaces,
llmRequests,
migrate,
Expand Down Expand Up @@ -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
Expand Down