o2o-infinith-demo/scripts/gen_rubric_v2_doc.ts
Haewon Kam c02de9ea51 feat(discovery): AEO/GEO 분리 채점 기준표 v2.0 초안 + 리포트 패널
- src/data/aeoGeoRubricV2.ts: AEO 답변 준비도 8항목·GEO 출처 준비도 8항목(각 100), 게이트 2종(크롤러 접근·본문 렌더링 → 상한 40), 부가 지표 11개
- v1.0 36항목을 v2 항목에 1:1 매핑(중복 0·미매핑 0, 스크립트 검사). 항목 점수 = v1 실측 레벨 가중 평균, 사람 판정(v2Results) 우선, 미검증은 분모 제외
- src/lib/discoveryScoreV2.ts 집계, src/components/discovery/AeoGeoV2Panel.tsx 리포트 패널(헤더 아래)
- 뷰성형외과 재채점: v1 54/C → AEO 42/C · GEO 56/C (사람 판정 4개는 수집기 원문 근거 초안). 태하: 게이트 G1(A2) 실패로 40 상한
- docs/AEO_GEO_RUBRIC_v2.md는 scripts/gen_rubric_v2_doc.ts로 코드에서 생성
- tsconfig exclude에 supporters·templates·evidence 추가 (Astro 하위 프로젝트가 루트 tsc에 잡히던 문제)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:12:35 +09:00

31 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { writeFileSync } from 'node:fs';
import { AEO_GEO_RUBRIC } from '../src/data/aeoGeoRubric';
import { AEO_GEO_RUBRIC_V2 } from '../src/data/aeoGeoRubricV2';
import { DISCOVERY_RESULTS } from '../src/data/discoveryResults';
import { scoreDiscovery } from '../src/lib/discoveryScore';
import { scoreDiscoveryV2, v1Usage } from '../src/lib/discoveryScoreV2';
const R = AEO_GEO_RUBRIC_V2; const v1 = new Map(AEO_GEO_RUBRIC.criteria.map((c) => [c.id, c]));
const M: Record<string, string> = { auto: '자동', semi: '반자동', manual: '사람' };
let md = `# INFINITH AI Discovery 채점 기준표 v2.0 (초안) · AEO / GEO 분리 지표\n\n> 코드 원본: \`src/data/aeoGeoRubricV2.ts\` · 집계: \`src/lib/discoveryScoreV2.ts\` · 타입: \`src/types/discovery.ts\` (v2 블록)\n> 상태: **${R.status}** (${R.updatedAt}). v1.0(36항목 종합 100점)은 유지하며 두 점수를 나란히 표시한다. 이 문서는 코드에서 생성한다(\`npx tsx scripts/gen_rubric_v2_doc.ts\`).\n\n`;
md += `## 설계\n\n- AEO 답변 준비도(100): 고객의 질문에 얼마나 명확하고 충분하게 답하는가.\n- GEO 출처 준비도(100): 그 답을 얼마나 검증하고 신뢰할 수 있는 출처로 사용할 수 있는가.\n- 같은 평가 항목을 두 점수에 중복 배정하지 않는다. v1.0 실측 항목 36개도 한 곳에만 매핑한다(아래 매핑표, 중복 0·미매핑 0을 스크립트로 검사).\n- 이 배점은 INFINITH 제품용 진단 기준이며, 검색엔진의 공식 점수나 인용 확률이 아니다.\n- 항목 점수 = 파생 신호(v1.0 실측 레벨 0~3)의 가중 평균. 사람 판정(\`v2Results\`)이 있으면 우선. 미검증은 분모 제외.\n- 게이트는 점수가 아니라 상한이다. 크롤러 차단·본문 미렌더면 두 축 모두 40 이하로 표시한다.\n- v1.0의 측정 인프라(E)·기술 위생 일부(F)·속도·hreflang·오픈웹 언급은 점수 밖 부가 지표로 남긴다.\n- 등급 경계는 v1.0과 같다: A(81+) · B(61–80) · C(41–60) · D(≤40).\n\n`;
for (const ax of R.axes) {
md += `## ${ax.code} · ${ax.name} (${ax.nameEn}) · 100\n\n${ax.question}\n\n| ID | 항목 | 배점 | 채점 | v1.0 파생 신호(비중) | 확인 방법 | 3점 기준 |\n|---|---|---|---|---|---|---|\n`;
for (const it of R.items.filter((i) => i.axis === ax.id)) md += `| ${it.id} | ${it.name} | ${it.weight} | ${M[it.method]} | ${it.derivedFrom.map((d) => `${d.criterionId} ${v1.get(d.criterionId)?.name ?? ''}(${d.weight})`).join(', ') || '없음 (사람 판정)'} | ${it.howToCheck} | ${it.levels[3].label} |\n`;
md += '\n';
}
md += `## 게이트 (상한)\n\n| ID | 이름 | v1.0 항목 | 실패 조건 | 상한 | 적용 축 | 이유 |\n|---|---|---|---|---|---|---|\n`;
for (const g of R.gates) md += `| ${g.id} | ${g.name} | ${g.criterionIds.join(', ')} | level ≤ ${g.failLevel} | ${g.cap} | ${g.axes.map((a) => a.toUpperCase()).join('·')} | ${g.reason} |\n`;
md += `\n## 점수 밖 부가 지표\n\n| v1.0 항목 | 그룹 |\n|---|---|\n`;
for (const a of R.auxiliary) md += `| ${a.criterionId} ${v1.get(a.criterionId)?.name ?? ''} | ${a.group} |\n`;
const usage = v1Usage(R);
md += `\n## v1.0 → v2.0 매핑표 (36항목 전수)\n\n| v1.0 | 항목 | 배점 | v2.0 위치 |\n|---|---|---|---|\n`;
for (const c of AEO_GEO_RUBRIC.criteria) md += `| ${c.id} | ${c.name} | ${c.weight} | ${(usage.get(c.id) ?? ['—']).join(', ')} |\n`;
md += `\n## 재채점 결과 (${R.updatedAt})\n\n| 업체 | v1.0 종합 | v2.0 AEO | v2.0 GEO | 게이트 | 비고 |\n|---|---|---|---|---|---|\n`;
for (const [id, r] of Object.entries(DISCOVERY_RESULTS)) { const a = scoreDiscovery(AEO_GEO_RUBRIC, r); const b = scoreDiscoveryV2(R, r); md += `| ${r.clinicName} (${id}) | ${a.score}/${a.grade} | ${b.aeo.score}/${b.aeo.grade} (검증 ${b.aeo.verifiedCount}/8${b.aeo.cappedBy.length ? ', 상한 적용' : ''}) | ${b.geo.score}/${b.geo.grade} (검증 ${b.geo.verifiedCount}/8${b.geo.cappedBy.length ? ', 상한 적용' : ''}) | ${b.gates.map((g) => `${g.gate.id} ${g.passed ? '통과' : '실패(' + g.failedBy.join(',') + ')'}`).join(' · ')} | 사람 판정 ${(r.v2Results ?? []).length}개 |\n`; }
md += `\n### 뷰성형외과 항목별\n\n| 항목 | 배점 | 점수 | 근거 | 신호 |\n|---|---|---|---|---|\n`;
const v = scoreDiscoveryV2(R, DISCOVERY_RESULTS.viewclinic);
for (const it of [...v.aeo.items, ...v.geo.items]) md += `| ${it.item.id} ${it.item.name} | ${it.item.weight} | ${it.pct === null ? '미검증' : `${it.earned} (${Math.round(it.pct * 100)}%)`} | ${it.basis === 'judged' ? '사람 판정' : it.basis === 'derived' ? 'v1 파생' : '—'} | ${it.signals.map((s) => `${s.criterionId}=${s.level}`).join(', ') || (it.evidence ?? '')} |\n`;
md += `\n## 해석 주의\n\n- v1.0 54/C(뷰성형외과)는 접근성·엔티티·콘텐츠·표면·측정을 한 점수로 합친 값이고, v2.0은 "답을 하는가"와 "출처가 되는가"를 나눈 값이라 서로 비교하지 않는다.\n- 사람 판정 항목(AEO6·GEO1·GEO6·GEO7)은 2026-09-07 수집기 원문을 근거로 한 초안이며 병원 확인 전이다.\n- 서포터즈 사이트를 같은 기준으로 채점하면 GEO 항목(근거 유형·검토자·확인일·정정 기록·지원 고지)이 설계 그대로 반영된다. 공식 홈페이지 vs 서포터즈 비교표는 이 기준으로 다시 만든다.\n`;
writeFileSync(new URL('../docs/AEO_GEO_RUBRIC_v2.md', import.meta.url), md);
console.log('doc written', md.length);