diff --git a/src/tools/council-archive/council-response-extractor.test.ts b/src/tools/council-archive/council-response-extractor.test.ts
index 03f2c1aa6..13a6c5f18 100644
--- a/src/tools/council-archive/council-response-extractor.test.ts
+++ b/src/tools/council-archive/council-response-extractor.test.ts
@@ -84,6 +84,75 @@ describe("extractCouncilResponse", () => {
})
})
+ describe("#given literal opening AND closing tags appear inside the actual response body", () => {
+ it("#then extracts the full structural response, not the literal mention", () => {
+ const text = [
+ "",
+ "## Finding 1: Triplicated Tag Constants",
+ "- Evidence:",
+ ' - `council-response-extractor.ts:1-2`: `export const OPENING_TAG = ""`, `export const CLOSING_TAG = ""`',
+ "## Finding 2: Another issue",
+ "Some more analysis text here.",
+ "",
+ ].join("\n")
+ const result = extractCouncilResponse(text)
+
+ expect(result).toEqual({
+ has_response: true,
+ response_complete: true,
+ result: [
+ "## Finding 1: Triplicated Tag Constants",
+ "- Evidence:",
+ ' - `council-response-extractor.ts:1-2`: `export const OPENING_TAG = ""`, `export const CLOSING_TAG = ""`',
+ "## Finding 2: Another issue",
+ "Some more analysis text here.",
+ ].join("\n"),
+ })
+ })
+ })
+
+ describe("#given multiple literal tag mentions scattered inside the response body", () => {
+ it("#then extracts the full structural response ignoring all literal mentions", () => {
+ const text = [
+ "preamble text",
+ "",
+ "The system uses for opening.",
+ "And for closing.",
+ "Also mentions again here.",
+ "Final analysis paragraph.",
+ "",
+ ].join("\n")
+ const result = extractCouncilResponse(text)
+
+ expect(result).toEqual({
+ has_response: true,
+ response_complete: true,
+ result: [
+ "The system uses for opening.",
+ "And for closing.",
+ "Also mentions again here.",
+ "Final analysis paragraph.",
+ ].join("\n"),
+ })
+ })
+ })
+
+ describe("#given a complete pair followed by a trailing incomplete opening tag", () => {
+ it("#then returns the incomplete trailing content as partial response", () => {
+ const text = [
+ "first complete",
+ "partial trailing content",
+ ].join("\n")
+ const result = extractCouncilResponse(text)
+
+ expect(result).toEqual({
+ has_response: true,
+ response_complete: false,
+ result: "partial trailing content",
+ })
+ })
+ })
+
describe("#given an empty string", () => {
it("#then returns has_response false and null result", () => {
const result = extractCouncilResponse("")
diff --git a/src/tools/council-archive/council-response-extractor.ts b/src/tools/council-archive/council-response-extractor.ts
index 67f0c91fa..6c5aba9a9 100644
--- a/src/tools/council-archive/council-response-extractor.ts
+++ b/src/tools/council-archive/council-response-extractor.ts
@@ -10,22 +10,104 @@ export interface CouncilResponseExtraction {
}
export function extractCouncilResponse(fullText: string): CouncilResponseExtraction {
- const lastOpenIdx = fullText.lastIndexOf(OPENING_TAG)
- if (lastOpenIdx === -1) {
- return { has_response: false, response_complete: false, result: null }
- }
+ const lastCloseIdx = findLastStructuralClose(fullText)
- const contentStart = lastOpenIdx + OPENING_TAG.length
- const closingAfterLastOpen = fullText.indexOf(CLOSING_TAG, contentStart)
-
- if (closingAfterLastOpen === -1) {
- const partial = fullText.slice(contentStart).trim()
+ if (lastCloseIdx === -1) {
+ const lastOpenIdx = findLastStructuralOpen(fullText)
+ if (lastOpenIdx === -1) {
+ return { has_response: false, response_complete: false, result: null }
+ }
+ const partial = fullText.slice(lastOpenIdx + OPENING_TAG.length).trim()
return { has_response: true, response_complete: false, result: partial || null }
}
- const content = fullText.slice(contentStart, closingAfterLastOpen).trim()
+ const openAfterLastClose = findFirstStructuralOpenAfter(fullText, lastCloseIdx + CLOSING_TAG.length)
+ if (openAfterLastClose !== -1) {
+ const partial = fullText.slice(openAfterLastClose + OPENING_TAG.length).trim()
+ return { has_response: true, response_complete: false, result: partial || null }
+ }
+
+ const matchingOpenIdx = findLastStructuralOpenBefore(fullText, lastCloseIdx)
+ if (matchingOpenIdx === -1) {
+ return { has_response: false, response_complete: false, result: null }
+ }
+
+ const content = fullText.slice(matchingOpenIdx + OPENING_TAG.length, lastCloseIdx).trim()
if (content.length < MIN_RESPONSE_LENGTH) {
return { has_response: false, response_complete: true, result: content }
}
return { has_response: true, response_complete: true, result: content }
}
+
+function isStructuralOpen(text: string, idx: number): boolean {
+ return idx === 0 || text[idx - 1] === "\n"
+}
+
+function isStructuralClose(text: string, idx: number): boolean {
+ const afterIdx = idx + CLOSING_TAG.length
+ return afterIdx === text.length || text[afterIdx] === "\n"
+}
+
+function findLastStructuralClose(text: string): number {
+ let searchFrom = text.length
+ while (searchFrom >= 0) {
+ const idx = text.lastIndexOf(CLOSING_TAG, searchFrom - 1)
+ if (idx === -1) return -1
+ if (isStructuralClose(text, idx)) return idx
+ searchFrom = idx
+ }
+ return -1
+}
+
+function findLastStructuralOpen(text: string): number {
+ let searchFrom = text.length
+ while (searchFrom >= 0) {
+ const idx = text.lastIndexOf(OPENING_TAG, searchFrom - 1)
+ if (idx === -1) return -1
+ if (isStructuralOpen(text, idx)) return idx
+ searchFrom = idx
+ }
+ return -1
+}
+
+function findFirstStructuralOpenAfter(text: string, fromIdx: number): number {
+ let searchFrom = fromIdx
+ while (searchFrom < text.length) {
+ const idx = text.indexOf(OPENING_TAG, searchFrom)
+ if (idx === -1) return -1
+ if (isStructuralOpen(text, idx)) return idx
+ searchFrom = idx + OPENING_TAG.length
+ }
+ return -1
+}
+
+function findLastStructuralOpenBefore(text: string, beforeIdx: number): number {
+ let searchFrom = beforeIdx
+ let nestedCloseCount = 0
+
+ while (searchFrom > 0) {
+ const openIdx = text.lastIndexOf(OPENING_TAG, searchFrom - 1)
+ const closeIdx = text.lastIndexOf(CLOSING_TAG, searchFrom - 1)
+
+ if (openIdx === -1 && closeIdx === -1) return -1
+
+ if (closeIdx > openIdx) {
+ if (isStructuralClose(text, closeIdx)) {
+ nestedCloseCount += 1
+ }
+ searchFrom = closeIdx
+ continue
+ }
+
+ if (!isStructuralOpen(text, openIdx)) {
+ searchFrom = openIdx
+ continue
+ }
+
+ if (nestedCloseCount === 0) return openIdx
+ nestedCloseCount -= 1
+ searchFrom = openIdx
+ }
+
+ return -1
+}