From 5e4af9646f10993e64b8a38bb1cdd8c0fdd05bec Mon Sep 17 00:00:00 2001 From: Subash Natarajan Date: Thu, 1 Oct 2026 17:03:58 +0530 Subject: [PATCH] Keep scoped approvals out of inferred engagement signers --- bin/fde.js | 22 ++++++++++++---- test/debrief-daily-updates.test.js | 40 ++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/bin/fde.js b/bin/fde.js index 7110355..c299bc1 100755 --- a/bin/fde.js +++ b/bin/fde.js @@ -1773,21 +1773,33 @@ function looksLikePersonName(s) { function signerFromLine(text) { const t = String(text || '').replace(/^[-*+]\s+/, '').trim() if (!t || /\?|\b(?:not|nobody|unclear|maybe|might|whether|could|should|if|unless|pending|unconfirmed)\b/i.test(t)) return '' + // A component/budget/release approver is not the engagement's acceptance + // signer. Infer only a bare authority statement or explicit outcome/test + // sign-off; leave other scope wording in the original note for agent review. + const roleWords = text => text.trim().split(/\s+/).every(word => word.match(ROLE_TOKEN)?.[0] === word) + const candidate = (match, who, rolePrefix = false) => { + if (!match || !looksLikePersonName(who)) return '' + const before = t.slice(0, match.index) + const after = t.slice(match.index + match[0].length).replace(/\[source:[^\]]*\]/gi, '').trim() + if (before.trim() && (!rolePrefix || !roleWords(before))) return '' + if (!/^(?:(?:on\s+)?(?:the\s+)?(?:acceptance tests?|(?:customer |delivered )?outcome))?[.!]?$/i.test(after)) return '' + return who.trim() + } // "Priya (VP Eng) signs off" → Priya. "Finance controller (Helena) signs off" → Helena. const titled = t.match(new RegExp('\\b' + SIGNER_NAME + '\\s+\\(' + SIGNER_NAME + '\\)\\s+' + SIGNER_VERB + '\\b')) if (titled) { const before = titled[1].trim() const inside = titled[2].trim() - if (looksLikePersonName(before) && ROLE_TOKEN.test(inside)) return before - if (looksLikePersonName(inside)) return inside - if (looksLikePersonName(before)) return before + if (looksLikePersonName(before) && roleWords(inside)) return candidate(titled, before) + if (roleWords(before) && looksLikePersonName(inside)) return candidate(titled, inside) + return '' } const paren = t.match(new RegExp('\\(' + SIGNER_NAME + '\\)\\s+' + SIGNER_VERB + '\\b')) - if (paren && looksLikePersonName(paren[1])) return paren[1].trim() + if (paren && looksLikePersonName(paren[1])) return candidate(paren, paren[1], true) const named = t.match(new RegExp('\\b' + SIGNER_NAME + '\\s+' + SIGNER_VERB + '\\b')) if (!named) return '' const who = named[1].trim() - return looksLikePersonName(who) ? who : '' + return candidate(named, who) } function setSigner(eng, who) { diff --git a/test/debrief-daily-updates.test.js b/test/debrief-daily-updates.test.js index b79ebd9..78131cb 100644 --- a/test/debrief-daily-updates.test.js +++ b/test/debrief-daily-updates.test.js @@ -78,6 +78,46 @@ test('explicit named signer retains source after sign-off prose is removed', t = assert.match(fs.readFileSync(path.join(f.eng, 'success.md'), 'utf8'), /\*\*Stakeholder who signs off:\*\* Mara Chen \[source: meeting:42\]/) assert.match(fs.readFileSync(path.join(f.eng, 'stakeholders.md'), 'utf8'), /Mara Chen \[source: meeting:42\] signs off/) }) +for (const statement of [ + 'Jo approves API compatibility only.', + 'Jo signs off on the budget.', + 'Jo has final say on the release.', + 'Finance controller (Jo) approves the budget.', + 'Jo (VP Eng) signs off on API compatibility.', + 'API compatibility is all Jo approves.', + 'Budget approver Jo signs off.', + 'Release manager Jo signs off.', + 'Jo signs off on the acceptance tests for API compatibility only.', +]) { + test(`scoped approval stays sourced context: ${statement}`, t => { + const f = fixture(t) + const files = ['success.md', 'stakeholders.md'] + const before = files.map(file => fs.readFileSync(path.join(f.eng, file), 'utf8')) + const note = `${statement} [source: meeting:scoped-42]` + const proposed = f.run(['debrief', '--smart'], note + '\n') + assert.equal(proposed.status, 0, proposed.stderr) + const proposal = fs.readFileSync(path.join(f.eng, '.debrief-propose'), 'utf8') + assert.doesNotMatch(proposal, /^signer:/m) + assert.ok(proposal.includes(note)) + assert.equal(f.run(['debrief', '--review']).status, 0) + assert.deepEqual(files.map(file => fs.readFileSync(path.join(f.eng, file), 'utf8')), before) + assert.equal(f.run(['debrief', '--apply']).status, 0) + assert.deepEqual(files.map(file => fs.readFileSync(path.join(f.eng, file), 'utf8')), before) + assert.ok(fs.readFileSync(path.join(f.eng, 'context.md'), 'utf8').includes(note)) + }) +} + +test('plural acceptance tests retain the sourced signer', t => { + const f = fixture(t) + const note = 'Jo signs off on the acceptance tests. [source: meeting:acceptance-42]' + const proposed = f.run(['debrief', '--smart'], note + '\n') + assert.equal(proposed.status, 0, proposed.stderr) + assert.match(fs.readFileSync(path.join(f.eng, '.debrief-propose'), 'utf8'), /^signer: Jo \[source: meeting:acceptance-42\]$/m) + assert.equal(f.run(['debrief', '--apply']).status, 0) + assert.match(fs.readFileSync(path.join(f.eng, 'success.md'), 'utf8'), /\*\*Stakeholder who signs off:\*\* Jo \[source: meeting:acceptance-42\]/) + assert.ok(fs.readFileSync(path.join(f.eng, 'context.md'), 'utf8').includes(note)) +}) + test('invalid delivery columns fail before changing records', t => { const f = fixture(t); const before = fs.readFileSync(path.join(f.eng, 'delivery.md'), 'utf8') assert.notEqual(f.run(['log', 'delivery', 'slice|promise|measured']).status, 0)