fix(desktop): align workstream card identity schema

Signed-off-by: loganj <loganj@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
loganj
2026-08-17 18:48:40 +00:00
co-authored by Codex
parent 3b948d8d72
commit b7c10d1851
4 changed files with 124 additions and 43 deletions
@@ -14,8 +14,11 @@ function withCardFence(prose, rawPayload) {
const VALID_PAYLOAD = {
version: 1,
synopsis: "Implementing the canvas card slice.",
orchestrator: "loganj",
assignees: ["alice", "bob"],
orchestrator: { pubkey: "loganj-pubkey", name: "Logan" },
assignees: [
{ pubkey: "alice-pubkey", name: "Alice" },
{ pubkey: "bob-pubkey", name: "Bob" },
],
};
// ── Happy path ────────────────────────────────────────────────────────────────
@@ -34,7 +37,7 @@ test("parses a valid v1 card with explicit optional arrays", () => {
version: 1,
synopsis: VALID_PAYLOAD.synopsis,
orchestrator: VALID_PAYLOAD.orchestrator,
assignees: ["alice", "bob"],
assignees: VALID_PAYLOAD.assignees,
pullRequests: ["https://github.com/block/buzz/pull/1"],
waitingOn: ["review"],
});
@@ -46,7 +49,7 @@ test("defaults assignees/pullRequests/waitingOn to empty arrays when omitted", (
);
assert.equal(result.ok, true);
assert.deepEqual(result.card.assignees, ["alice", "bob"]);
assert.deepEqual(result.card.assignees, VALID_PAYLOAD.assignees);
assert.deepEqual(result.card.pullRequests, []);
assert.deepEqual(result.card.waitingOn, []);
});
@@ -144,10 +147,10 @@ test("returns invalid-fields when synopsis is missing", () => {
});
});
test("returns invalid-fields when orchestrator is an empty string", () => {
test("returns invalid-fields when orchestrator is missing its identity fields", () => {
const content = withCardFence("Status:", {
...VALID_PAYLOAD,
orchestrator: "",
orchestrator: { name: "Logan" },
});
assert.deepEqual(parseWorkstreamCard(content), {
ok: false,
@@ -166,10 +169,10 @@ test("returns invalid-fields when assignees is not an array", () => {
});
});
test("returns invalid-fields when assignees contains a non-string", () => {
test("returns invalid-fields when assignees contains an invalid identity", () => {
const content = withCardFence("Status:", {
...VALID_PAYLOAD,
assignees: ["alice", 2],
assignees: [{ pubkey: "alice-pubkey", name: "Alice" }, "bob"],
});
assert.deepEqual(parseWorkstreamCard(content), {
ok: false,
@@ -199,6 +202,28 @@ test("returns invalid-fields when waitingOn is not an array", () => {
});
});
test("rejects the former string identity schema for orchestrator", () => {
const content = withCardFence("Status:", {
...VALID_PAYLOAD,
orchestrator: "loganj",
});
assert.deepEqual(parseWorkstreamCard(content), {
ok: false,
reason: "invalid-fields",
});
});
test("rejects the former string identity schema for assignees", () => {
const content = withCardFence("Status:", {
...VALID_PAYLOAD,
assignees: ["alice"],
});
assert.deepEqual(parseWorkstreamCard(content), {
ok: false,
reason: "invalid-fields",
});
});
test("returns invalid-fields when the payload is a JSON array, not an object", () => {
const content = withCardFence("Status:", [VALID_PAYLOAD]);
assert.deepEqual(parseWorkstreamCard(content), {
@@ -206,3 +231,25 @@ test("returns invalid-fields when the payload is a JSON array, not an object", (
reason: "invalid-fields",
});
});
test("returns invalid-fields when synopsis spans multiple lines", () => {
const content = withCardFence("Status:", {
...VALID_PAYLOAD,
synopsis: "line one\nline two",
});
assert.deepEqual(parseWorkstreamCard(content), {
ok: false,
reason: "invalid-fields",
});
});
test("returns invalid-fields when optional arrays are explicitly null", () => {
const content = withCardFence("Status:", {
...VALID_PAYLOAD,
pullRequests: null,
});
assert.deepEqual(parseWorkstreamCard(content), {
ok: false,
reason: "invalid-fields",
});
});
@@ -6,7 +6,7 @@
*
* ```
* ```buzz-workstream-card
* {"version":1,"synopsis":"…","orchestrator":"…","assignees":[…]}
* {"version":1,"synopsis":"…","orchestrator":{"pubkey":"…","name":"…"},"assignees":[{"pubkey":"…","name":"…"}]}
* ```
* ```
*
@@ -19,11 +19,16 @@
const FENCE_OPEN = "```buzz-workstream-card";
const FENCE_CLOSE = "```";
export type WorkstreamIdentity = {
pubkey: string;
name: string;
};
export type WorkstreamCardV1 = {
version: 1;
synopsis: string;
orchestrator: string;
assignees: string[];
orchestrator: WorkstreamIdentity;
assignees: WorkstreamIdentity[];
pullRequests: unknown[];
waitingOn: unknown[];
};
@@ -40,30 +45,51 @@ export type WorkstreamCardParseResult =
| { ok: false; reason: WorkstreamCardParseFailureReason };
function findFencedBlocks(content: string): string[] {
const lines = content.split(/\r?\n/);
const blocks: string[] = [];
let cursor = 0;
while (true) {
const openIdx = content.indexOf(FENCE_OPEN, cursor);
if (openIdx === -1) break;
for (let index = 0; index < lines.length; index += 1) {
if (lines[index].trimEnd() !== FENCE_OPEN) continue;
const jsonStart = content.indexOf("\n", openIdx);
if (jsonStart === -1) break;
const body: string[] = [];
let closeIndex = index + 1;
while (
closeIndex < lines.length &&
lines[closeIndex].trim() !== FENCE_CLOSE
) {
body.push(lines[closeIndex]);
closeIndex += 1;
}
if (closeIndex === lines.length) break;
const closeIdx = content.indexOf(`\n${FENCE_CLOSE}`, jsonStart);
if (closeIdx === -1) break;
blocks.push(content.slice(jsonStart + 1, closeIdx).trim());
cursor = closeIdx + `\n${FENCE_CLOSE}`.length;
blocks.push(body.join("\n").trim());
index = closeIndex;
}
return blocks;
}
function isStringArray(value: unknown): value is string[] {
return (
Array.isArray(value) && value.every((item) => typeof item === "string")
);
function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim() !== "";
}
function isOneLineString(value: unknown): value is string {
return isNonEmptyString(value) && !/[\r\n]/.test(value);
}
function isWorkstreamIdentity(value: unknown): value is WorkstreamIdentity {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return false;
}
const raw = value as Record<string, unknown>;
return isNonEmptyString(raw.pubkey) && isNonEmptyString(raw.name);
}
function isWorkstreamIdentityArray(
value: unknown,
): value is WorkstreamIdentity[] {
return Array.isArray(value) && value.every(isWorkstreamIdentity);
}
/**
@@ -102,24 +128,27 @@ export function parseWorkstreamCard(
return { ok: false, reason: "unknown-version" };
}
if (typeof raw.synopsis !== "string" || raw.synopsis.trim() === "") {
return { ok: false, reason: "invalid-fields" };
}
if (typeof raw.orchestrator !== "string" || raw.orchestrator.trim() === "") {
const synopsis = raw.synopsis;
if (!isOneLineString(synopsis)) {
return { ok: false, reason: "invalid-fields" };
}
const assignees = raw.assignees ?? [];
if (!isStringArray(assignees)) {
const orchestrator = raw.orchestrator;
if (!isWorkstreamIdentity(orchestrator)) {
return { ok: false, reason: "invalid-fields" };
}
const pullRequests = raw.pullRequests ?? [];
const assignees = raw.assignees === undefined ? [] : raw.assignees;
if (!isWorkstreamIdentityArray(assignees)) {
return { ok: false, reason: "invalid-fields" };
}
const pullRequests = raw.pullRequests === undefined ? [] : raw.pullRequests;
if (!Array.isArray(pullRequests)) {
return { ok: false, reason: "invalid-fields" };
}
const waitingOn = raw.waitingOn ?? [];
const waitingOn = raw.waitingOn === undefined ? [] : raw.waitingOn;
if (!Array.isArray(waitingOn)) {
return { ok: false, reason: "invalid-fields" };
}
@@ -128,8 +157,8 @@ export function parseWorkstreamCard(
ok: true,
card: {
version: 1,
synopsis: raw.synopsis,
orchestrator: raw.orchestrator,
synopsis,
orchestrator,
assignees,
pullRequests,
waitingOn,
@@ -8,8 +8,8 @@ const VALID_CARD_CONTENT = [
JSON.stringify({
version: 1,
synopsis: "Shipping the canvas card slice.",
orchestrator: "loganj",
assignees: ["alice"],
orchestrator: { pubkey: "loganj-pubkey", name: "Logan" },
assignees: [{ pubkey: "alice-pubkey", name: "Alice" }],
}),
"```",
].join("\n");
@@ -72,8 +72,13 @@ test("returns a ready card when the canvas parses successfully", () => {
assert.equal(viewModel.status, "ready");
assert.equal(viewModel.card.synopsis, "Shipping the canvas card slice.");
assert.equal(viewModel.card.orchestrator, "loganj");
assert.deepEqual(viewModel.card.assignees, ["alice"]);
assert.deepEqual(viewModel.card.orchestrator, {
pubkey: "loganj-pubkey",
name: "Logan",
});
assert.deepEqual(viewModel.card.assignees, [
{ pubkey: "alice-pubkey", name: "Alice" },
]);
});
test("loading takes priority over content even if content happens to be malformed", () => {
@@ -48,7 +48,7 @@ export function WorkstreamCard({ channel, onSelect }: WorkstreamCardProps) {
<p className="truncate text-2xs text-muted-foreground">
Orchestrator:{" "}
<span className="text-foreground">
{viewModel.card.orchestrator}
{viewModel.card.orchestrator.name}
</span>
</p>
{viewModel.card.assignees.length > 0 ? (
@@ -56,9 +56,9 @@ export function WorkstreamCard({ channel, onSelect }: WorkstreamCardProps) {
{viewModel.card.assignees.map((assignee) => (
<span
className="rounded-full border border-border/65 bg-background/80 px-2 py-0.5 text-2xs text-muted-foreground"
key={assignee}
key={assignee.pubkey}
>
{assignee}
{assignee.name}
</span>
))}
</div>