Skip to content

Commit b0d88e7

Browse files
committed
fix(architect): install fix & guard native plan verbosity parsing
1 parent 5dcd0b3 commit b0d88e7

5 files changed

Lines changed: 181 additions & 8 deletions

File tree

extensions/vscode/package-lock.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/vscode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,6 @@
444444
"dompurify": "^3.4.0",
445445
"markdown-it": "^14.1.1",
446446
"mermaid": "^11.12.0",
447-
"@dreamgraph/token-economy": "12.4.0"
447+
"@dreamgraph/token-economy": "file:../../packages/token-economy"
448448
}
449449
}

src/architect/plan-registry.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -690,10 +690,16 @@ function classifySlice(title: string): { category: "phase" | "slice"; sortId: st
690690
if (phaseMatch) {
691691
return { category: "phase", sortId: `phase-${phaseMatch[1]}` };
692692
}
693-
const sliceMatch = title.match(/^Slice\s+([A-Za-z0-9]+)[.:]?\s*(.*)$/i);
694-
if (sliceMatch) {
695-
const ordinal = sliceMatch[1].toLowerCase();
696-
const titleSlug = slugifySliceSegment(sliceMatch[2] ?? "");
693+
const numericSliceMatch = title.match(/^Slice\s+(\d+)[.:]?\s*(.*)$/i);
694+
if (numericSliceMatch) {
695+
const ordinal = numericSliceMatch[1].toLowerCase();
696+
const titleSlug = slugifySliceSegment(numericSliceMatch[2] ?? "");
697+
return { category: "slice", sortId: titleSlug ? `slice-${ordinal}-${titleSlug}` : `slice-${ordinal}` };
698+
}
699+
const letterSliceMatch = title.match(/^Slice\s+([A-Z])(?:[.:]|\s+[-])\s*(.*)$/i);
700+
if (letterSliceMatch) {
701+
const ordinal = letterSliceMatch[1].toLowerCase();
702+
const titleSlug = slugifySliceSegment(letterSliceMatch[2] ?? "");
697703
return { category: "slice", sortId: titleSlug ? `slice-${ordinal}-${titleSlug}` : `slice-${ordinal}` };
698704
}
699705
return null;

src/architect/routes.ts

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,7 +1556,9 @@ function buildCreatedPlanLog(planId: string, title: string, timestamp: string):
15561556
}
15571557

15581558
function isArchitectRuntimeIdentityQuestion(message: string): boolean {
1559-
return /\b(model|provider|adapter|runtime|route|session)\b/i.test(message);
1559+
const compact = message.replace(/\s+/g, " ").trim();
1560+
if (!compact.endsWith("?")) return false;
1561+
return /\b(what|which|who)\b.*\b(model|provider|adapter|runtime|route|session)\b/i.test(compact);
15601562
}
15611563

15621564
function shouldApplyPlanChatUpdate(message: string, plan: ArchitectPlanProjection | null): boolean {
@@ -1576,11 +1578,101 @@ function normalizePlanChatUpdateContent(message: string): string {
15761578
.slice(0, 24_000);
15771579
}
15781580

1581+
function splitMarkdownTableRow(line: string): string[] {
1582+
return line
1583+
.trim()
1584+
.replace(/^\|/, "")
1585+
.replace(/\|$/, "")
1586+
.split("|")
1587+
.map((cell) => cell.trim());
1588+
}
1589+
1590+
function isMarkdownTableDivider(line: string): boolean {
1591+
return /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);
1592+
}
1593+
1594+
function normalizeSliceTitle(sliceCell: string, changeCell: string): string {
1595+
const cleanSlice = sliceCell.replace(/^slice\s+/i, "").trim();
1596+
const ordinal = cleanSlice || "1";
1597+
const firstSentence = changeCell
1598+
.replace(/`([^`]+)`/g, "$1")
1599+
.split(/[.!?]/)[0]
1600+
?.replace(/\s+/g, " ")
1601+
.trim();
1602+
const title = firstSentence && firstSentence.length > 0 ? firstSentence.slice(0, 80) : "Implementation Work";
1603+
return `Slice ${ordinal} - ${title}`;
1604+
}
1605+
1606+
function normalizeArchitectNativePlanMarkdown(markdown: string): string {
1607+
const lines = markdown.split("\n");
1608+
const nextLines: string[] = [];
1609+
for (let index = 0; index < lines.length; index += 1) {
1610+
const headingMatch = lines[index].match(/^(##\s+)(\d+\.\s+)?Planned Slices\s*$/i);
1611+
if (!headingMatch) {
1612+
nextLines.push(lines[index]);
1613+
continue;
1614+
}
1615+
1616+
nextLines.push(`${headingMatch[1]}${headingMatch[2] ?? ""}Implementation Slices`);
1617+
index += 1;
1618+
const sectionLines: string[] = [];
1619+
while (index < lines.length && !/^##\s+/.test(lines[index])) {
1620+
sectionLines.push(lines[index]);
1621+
index += 1;
1622+
}
1623+
index -= 1;
1624+
1625+
const tableHeaderIndex = sectionLines.findIndex((line) => /^\s*\|\s*Slice\s*\|/i.test(line));
1626+
if (tableHeaderIndex < 0 || tableHeaderIndex + 1 >= sectionLines.length || !isMarkdownTableDivider(sectionLines[tableHeaderIndex + 1])) {
1627+
nextLines.push(...sectionLines);
1628+
continue;
1629+
}
1630+
1631+
nextLines.push(...sectionLines.slice(0, tableHeaderIndex));
1632+
const headers = splitMarkdownTableRow(sectionLines[tableHeaderIndex]).map((cell) => cell.toLowerCase());
1633+
const sliceIndex = headers.indexOf("slice");
1634+
const changeIndex = headers.indexOf("change");
1635+
const providerIndex = headers.findIndex((cell) => cell.includes("provider") || cell.includes("adapter"));
1636+
const filesIndex = headers.indexOf("files");
1637+
const verificationIndex = headers.indexOf("verification");
1638+
1639+
for (let tableIndex = tableHeaderIndex + 2; tableIndex < sectionLines.length; tableIndex += 1) {
1640+
const line = sectionLines[tableIndex];
1641+
if (!/^\s*\|/.test(line)) {
1642+
if (line.trim()) nextLines.push(line);
1643+
continue;
1644+
}
1645+
const cells = splitMarkdownTableRow(line);
1646+
const sliceCell = sliceIndex >= 0 ? cells[sliceIndex] ?? "" : "";
1647+
const changeCell = changeIndex >= 0 ? cells[changeIndex] ?? "" : "";
1648+
if (!sliceCell || !changeCell) continue;
1649+
1650+
nextLines.push("");
1651+
nextLines.push(`### ${normalizeSliceTitle(sliceCell, changeCell)}`);
1652+
nextLines.push("");
1653+
nextLines.push(changeCell);
1654+
if (providerIndex >= 0 && cells[providerIndex]) {
1655+
nextLines.push("");
1656+
nextLines.push(`Provider/adapter specifics: ${cells[providerIndex]}`);
1657+
}
1658+
if (filesIndex >= 0 && cells[filesIndex]) {
1659+
nextLines.push("");
1660+
nextLines.push(`Files: ${cells[filesIndex]}`);
1661+
}
1662+
if (verificationIndex >= 0 && cells[verificationIndex]) {
1663+
nextLines.push("");
1664+
nextLines.push(`Verification: ${cells[verificationIndex]}`);
1665+
}
1666+
}
1667+
}
1668+
return nextLines.join("\n");
1669+
}
1670+
15791671
function applyPlanChatUpdateToMarkdown(markdown: string, content: string, timestamp: string): { markdown: string; mode: ArchitectPlanChatUpdateResult["update_mode"]; sectionTitle: string } {
15801672
const placeholder = "Describe the goal for this Architect plan.";
15811673
if (markdown.includes(placeholder)) {
15821674
return {
1583-
markdown: markdown.replace(placeholder, content),
1675+
markdown: normalizeArchitectNativePlanMarkdown(markdown.replace(placeholder, content)),
15841676
mode: "replace_goal_placeholder",
15851677
sectionTitle: "0. Goal",
15861678
};
@@ -1597,7 +1689,7 @@ function applyPlanChatUpdateToMarkdown(markdown: string, content: string, timest
15971689
content,
15981690
"",
15991691
].join("\n");
1600-
return { markdown: nextMarkdown, mode: "append_chat_update", sectionTitle };
1692+
return { markdown: normalizeArchitectNativePlanMarkdown(nextMarkdown), mode: "append_chat_update", sectionTitle };
16011693
}
16021694

16031695
async function applyPlanChatUpdate(

tests/standalone-architect-routes.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,72 @@ describe("standalone Architect route hardening", () => {
11481148
}
11491149
});
11501150

1151+
it("normalizes chat-authored slice tables into native implementation slice headings", async () => {
1152+
const tempRoot = await mkdtemp(join(tmpdir(), "dreamgraph-plan-slice-table-"));
1153+
const plansDir = join(tempRoot, "plans");
1154+
const dataDir = join(tempRoot, "data");
1155+
const scopeSpy = vi.spyOn(lifecycle, "getActiveScope").mockReturnValue({
1156+
instanceId: "test",
1157+
projectRoot: tempRoot,
1158+
plansRoot: plansDir,
1159+
dataDir,
1160+
unsafeMode: false,
1161+
source: "test",
1162+
});
1163+
1164+
try {
1165+
await mkdir(plansDir, { recursive: true });
1166+
await mkdir(dataDir, { recursive: true });
1167+
1168+
await withArchitectServer(async (baseUrl) => {
1169+
const createdResponse = await fetch(`${baseUrl}/api/architect/v1/plans`, {
1170+
method: "POST",
1171+
headers: { "Content-Type": "application/json" },
1172+
body: JSON.stringify({ title: "Slice Table Plan" }),
1173+
});
1174+
expect(createdResponse.status).toBe(201);
1175+
const created = await createdResponse.json() as Record<string, unknown>;
1176+
const createdResult = created.result as Record<string, unknown>;
1177+
const planId = String(createdResult.plan_id);
1178+
1179+
await expectJsonOk(await fetch(`${baseUrl}/api/architect/v1/chat`, {
1180+
method: "POST",
1181+
headers: { "Content-Type": "application/json" },
1182+
body: JSON.stringify({
1183+
planId,
1184+
adapter: "codex-cli",
1185+
provider: "none",
1186+
model: "gpt-5.4",
1187+
mode: "autonomous",
1188+
message: [
1189+
"this plan should include implementation slices",
1190+
"",
1191+
"## 3. Planned Slices",
1192+
"",
1193+
"| Slice | Change | Provider/adapter specifics | Files | Verification |",
1194+
"| --- | --- | --- | --- | --- |",
1195+
"| 1 | Build the parser contract. | Applies to every route. | `src/architect/routes.ts` | Registry reports Slice 1. |",
1196+
"| 2 | Add regression coverage. | Test-only. | `tests/standalone-architect-routes.test.ts` | Test passes. |",
1197+
].join("\n"),
1198+
}),
1199+
}));
1200+
1201+
const markdown = await readFile(join(plansDir, `${planId}.md`), "utf-8");
1202+
expect(markdown).toContain("## 3. Implementation Slices");
1203+
expect(markdown).toContain("### Slice 1 - Build the parser contract");
1204+
expect(markdown).toContain("### Slice 2 - Add regression coverage");
1205+
expect(markdown).not.toContain("| Slice | Change |");
1206+
1207+
const projected = await expectJsonOk(await fetch(`${baseUrl}/api/architect/v1/plans/${planId}`));
1208+
const plan = projected.plan as { registry?: { summary?: { slice_count?: number } } };
1209+
expect(plan.registry?.summary?.slice_count).toBe(2);
1210+
});
1211+
} finally {
1212+
scopeSpy.mockRestore();
1213+
await rm(tempRoot, { recursive: true, force: true });
1214+
}
1215+
});
1216+
11511217
it("rejects GET chat prompts and supports POST SSE chat responses", async () => {
11521218
await withArchitectServer(async (baseUrl) => {
11531219
const rejected = await fetch(`${baseUrl}/api/architect/v1/chat?message=must-not-travel-in-url`);

0 commit comments

Comments
 (0)