v2.2.258: /meet 화자 팀/역할 정규화 + 헤더 전조각 주입 + 검증 5종
STT 화자번호(참석자 N) 박멸→회사 표준 팀/역할 귀속, 회의 헤더 전 청크 주입, 전역 헤드라인 추출, 결정 게이트·담화 상태 태깅, 슬림 6섹션 포맷, 타임스탬프 근거, parseActionItems 헤더명 기반 재작성, 검증 패스 5종. 전체 698 통과. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+40
-25
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
* Regression guard for the /meet prompt policy.
|
||||
* Regression guard for the /meet prompt policy (v2.2.258).
|
||||
*
|
||||
* These 4 rules were added after a real meeting record showed the failure modes:
|
||||
* 1) anonymous/numbered speakers assigned as action owners (fake accountability)
|
||||
* 2) content-empty deictic quotes ("이렇게 이렇게") passed off as 근거
|
||||
* 3) deadlines mentioned in risks/discussion not reflected in action 기한
|
||||
* 4) speakers in the body who aren't in the attendee list
|
||||
* After a real meeting record review, the policy shifted to:
|
||||
* 1) STT speaker numbers ("참석자 N") must NEVER appear in output — speakers are
|
||||
* normalized to team/role instead (individual names only when certain).
|
||||
* 2) empty cells are "—", not guessed placeholders; fully-undecided → open issue.
|
||||
* 3) 근거 is a timestamp [mm:ss], not a raw STT quote.
|
||||
* 4) decision vs action boundary: 일감→action table, 결정→pure direction only.
|
||||
* 5) rejected/withdrawn hypotheses must not be promoted to issues/decisions.
|
||||
*
|
||||
* Prompts can't be unit-tested for model behavior, but we CAN guard that the
|
||||
* policy text doesn't silently get dropped in a future refactor.
|
||||
@@ -17,44 +19,57 @@ import {
|
||||
buildMeetReducePrompt,
|
||||
} from '../src/features/datacollect/prompts/meetPrompt';
|
||||
|
||||
const transcript = '참석자 1: 이거는 이렇게 이렇게 해주세요. 효희 팀장: 네 수정할게요.';
|
||||
const transcript = '참석자 1 00:01 이거는 이렇게 이렇게 해주세요. 참석자 2 00:10 네 수정할게요.';
|
||||
const metadata = '회의명: 테스트';
|
||||
|
||||
describe('/meet prompt — accountability & grounding policy', () => {
|
||||
describe('/meet prompt — speaker normalization & slim format policy', () => {
|
||||
// The OUTPUT_FORMAT block is shared by the single-shot and reduce paths, so
|
||||
// both must carry the policy.
|
||||
const sharedFormatBuilders: Array<[string, string]> = [
|
||||
['buildMeetPrompt', buildMeetPrompt(transcript, metadata)],
|
||||
['buildMeetReducePrompt', buildMeetReducePrompt('## 액션\n- [미지정] 작업', metadata)],
|
||||
['buildMeetReducePrompt', buildMeetReducePrompt('## 액션\n- [넥서스개발팀] 작업', metadata)],
|
||||
];
|
||||
|
||||
test.each(sharedFormatBuilders)('%s forbids assigning anonymous speakers as owners', (_name, prompt) => {
|
||||
expect(prompt).toContain('[미지정-확인필요]');
|
||||
expect(prompt).toMatch(/익명·번호 화자/);
|
||||
test.each(sharedFormatBuilders)('%s bans "참석자 N" tokens in output', (_name, prompt) => {
|
||||
expect(prompt).toMatch(/"참석자 N"/);
|
||||
expect(prompt).toMatch(/토큰 0개|절대 금지|절대 쓰지/);
|
||||
});
|
||||
|
||||
test.each(sharedFormatBuilders)('%s rejects content-empty deictic quotes', (_name, prompt) => {
|
||||
expect(prompt).toContain('[내용 확인필요]');
|
||||
expect(prompt).toMatch(/이렇게 이렇게/);
|
||||
test.each(sharedFormatBuilders)('%s attributes speakers by team/role', (_name, prompt) => {
|
||||
expect(prompt).toMatch(/팀\/역할/);
|
||||
});
|
||||
|
||||
test.each(sharedFormatBuilders)('%s cross-references deadlines into action 기한', (_name, prompt) => {
|
||||
expect(prompt).toMatch(/기한 역참조/);
|
||||
test.each(sharedFormatBuilders)('%s requires timestamp 근거 ([mm:ss])', (_name, prompt) => {
|
||||
expect(prompt).toMatch(/\[mm:ss\]/);
|
||||
});
|
||||
|
||||
test.each(sharedFormatBuilders)('%s flags speakers missing from the attendee list', (_name, prompt) => {
|
||||
expect(prompt).toMatch(/명단 외 화자|참석자 명단 정합성/);
|
||||
test.each(sharedFormatBuilders)('%s separates decision from action', (_name, prompt) => {
|
||||
expect(prompt).toMatch(/결정 ↔ 액션 경계|순수 방향\/정책|결정\/액션 경계/);
|
||||
});
|
||||
|
||||
test('extract (map) stage also avoids anonymous owner assignment & empty quotes', () => {
|
||||
const p = buildMeetExtractPrompt('참석자 1: 이거 이렇게요', metadata, 1, 3);
|
||||
expect(p).toContain('[미지정]');
|
||||
expect(p).toContain('[내용 확인필요]');
|
||||
test.each(sharedFormatBuilders)('%s blanks unknown cells with "—" instead of guessing', (_name, prompt) => {
|
||||
expect(prompt).toMatch(/빈 칸은 "—"/);
|
||||
});
|
||||
|
||||
test('extract (map) stage normalizes speakers to role, tags dialectic state & timestamps', () => {
|
||||
const p = buildMeetExtractPrompt('참석자 1 00:01 이거 이렇게요', metadata, 1, 3);
|
||||
expect(p).toMatch(/팀\/역할/); // role mapping
|
||||
expect(p).toMatch(/\[mm:ss\]/); // timestamp grounding
|
||||
expect(p).toMatch(/반박됨|철회됨/); // dialectic state tags
|
||||
expect(p).toMatch(/안건\/주제/); // agenda coverage list
|
||||
expect(p).toContain('[내용 확인필요]'); // content-empty deictic guard retained
|
||||
});
|
||||
|
||||
test('reduce stage extracts a global headline & dedups across chunks', () => {
|
||||
const p = buildMeetReducePrompt('## 액션\n- [기획] 작업', metadata);
|
||||
expect(p).toMatch(/전역 헤드라인/);
|
||||
expect(p).toMatch(/dedup|병합/);
|
||||
expect(p).toMatch(/반박됨|폐기/); // rejected hypotheses not promoted
|
||||
});
|
||||
|
||||
test('final checklist references the new gates', () => {
|
||||
const prompt = buildMeetPrompt(transcript, metadata);
|
||||
expect(prompt).toMatch(/익명·번호 화자를 담당으로 확정하지 않고/);
|
||||
expect(prompt).toMatch(/빈 인용을 근거로 달지 않았는가/);
|
||||
expect(prompt).toMatch(/"참석자 N" 토큰 0개/);
|
||||
expect(prompt).toMatch(/빈 칸은 "—"/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,8 +111,26 @@ describe('보조 유틸', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseActionItems — 액션 표 파싱 (산출물 컬럼 + 하위호환)', () => {
|
||||
test('신 형식 6컬럼: 담당|작업|상세|산출물|기한|상태', () => {
|
||||
describe('parseActionItems — 액션 표 파싱 (헤더명 기반 매핑 + 하위호환)', () => {
|
||||
test('신 형식 5컬럼(v2.2.258): 담당|액션|기한|상태|출처 — 컬럼 순서가 달라도 안전', () => {
|
||||
const report = [
|
||||
'## 액션 아이템',
|
||||
'| 담당 | 액션 | 기한 | 상태 | 출처 |',
|
||||
'| --- | --- | --- | --- | --- |',
|
||||
'| 넥서스개발팀 | 모자 헤어스타일 단정 버전 3종 시안 제작 | 6/18 | 확정 | [07:14] |',
|
||||
'| — | 모델 교체 소요기간 산정 | — | 진행미정 | [53:55] |',
|
||||
'',
|
||||
'## 오픈 이슈',
|
||||
].join('\n');
|
||||
const rows = parseActionItems(report);
|
||||
expect(rows).toHaveLength(2);
|
||||
// 출처가 기한/상태 뒤에 와도 이름으로 매핑 → 어긋나지 않는다
|
||||
expect(rows[0]).toEqual({ owner: '넥서스개발팀', work: '모자 헤어스타일 단정 버전 3종 시안 제작', detail: '', deliverable: '', due: '6/18', status: '확정', source: '[07:14]' });
|
||||
// '—' 는 미정 신호 → 빈 문자열로 정규화
|
||||
expect(rows[1]).toMatchObject({ owner: '', work: '모델 교체 소요기간 산정', due: '', status: '진행미정', source: '[53:55]' });
|
||||
});
|
||||
|
||||
test('구 형식 6컬럼: 담당|작업|상세|산출물|기한|상태', () => {
|
||||
const report = [
|
||||
'## 4. 액션 아이템',
|
||||
'| 담당 | 작업 내용 | 작업 상세 | 산출물 | 기한 | 상태 |',
|
||||
@@ -123,7 +141,7 @@ describe('parseActionItems — 액션 표 파싱 (산출물 컬럼 + 하위호
|
||||
].join('\n');
|
||||
const rows = parseActionItems(report);
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toEqual({ owner: '송병준', work: '테스트 샘플 3종 선정', detail: '후보 비교', deliverable: '테스트 URL 목록', due: '6/18', status: '확정' });
|
||||
expect(rows[0]).toEqual({ owner: '송병준', work: '테스트 샘플 3종 선정', detail: '후보 비교', deliverable: '테스트 URL 목록', due: '6/18', status: '확정', source: '' });
|
||||
});
|
||||
|
||||
test('구 형식 5컬럼(산출물 없음)도 그대로 파싱 — deliverable 빈값', () => {
|
||||
|
||||
Reference in New Issue
Block a user