fix(council-archive): handle literal tag mentions inside response content
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -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 = [
|
||||
"<COUNCIL_MEMBER_RESPONSE>",
|
||||
"## Finding 1: Triplicated Tag Constants",
|
||||
"- Evidence:",
|
||||
' - `council-response-extractor.ts:1-2`: `export const OPENING_TAG = "<COUNCIL_MEMBER_RESPONSE>"`, `export const CLOSING_TAG = "</COUNCIL_MEMBER_RESPONSE>"`',
|
||||
"## Finding 2: Another issue",
|
||||
"Some more analysis text here.",
|
||||
"</COUNCIL_MEMBER_RESPONSE>",
|
||||
].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 = "<COUNCIL_MEMBER_RESPONSE>"`, `export const CLOSING_TAG = "</COUNCIL_MEMBER_RESPONSE>"`',
|
||||
"## 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",
|
||||
"<COUNCIL_MEMBER_RESPONSE>",
|
||||
"The system uses <COUNCIL_MEMBER_RESPONSE> for opening.",
|
||||
"And </COUNCIL_MEMBER_RESPONSE> for closing.",
|
||||
"Also mentions <COUNCIL_MEMBER_RESPONSE> again here.",
|
||||
"Final analysis paragraph.",
|
||||
"</COUNCIL_MEMBER_RESPONSE>",
|
||||
].join("\n")
|
||||
const result = extractCouncilResponse(text)
|
||||
|
||||
expect(result).toEqual({
|
||||
has_response: true,
|
||||
response_complete: true,
|
||||
result: [
|
||||
"The system uses <COUNCIL_MEMBER_RESPONSE> for opening.",
|
||||
"And </COUNCIL_MEMBER_RESPONSE> for closing.",
|
||||
"Also mentions <COUNCIL_MEMBER_RESPONSE> 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 = [
|
||||
"<COUNCIL_MEMBER_RESPONSE>first complete</COUNCIL_MEMBER_RESPONSE>",
|
||||
"<COUNCIL_MEMBER_RESPONSE>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("")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user