- 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>
31 lines
5.4 KiB
TypeScript
31 lines
5.4 KiB
TypeScript
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);
|