report-statistics.js 2.3 KB

123456789101112131415161718192021222324252627282930313233
  1. // Generated by scripts/build-workshop-skill.mjs; maintain the Skill report-statistics.mjs source.
  2. /** A semantic table within the existing report schema; no new section.kind or API fields. */
  3. export const STATISTICS_SECTION_ID = 'statistics-summary';
  4. export const STATISTICS_COLUMNS = Object.freeze(['维度/主题', '提及数 n', '有效样本 N', '统计口径与范围', '数据解释与限制', '产品含义', '证据']);
  5. const invalid = () => { throw Object.assign(new Error('REPORT_STATISTICS_INVALID'), { code: 'REPORT_STATISTICS_INVALID' }); };
  6. const hasText = value => typeof value === 'string' && Boolean(value.trim());
  7. const count = value => value === null || Number.isSafeInteger(value) && value >= 0;
  8. // The board's CJK typography pass may replace ASCII spaces with thin spaces.
  9. const columnKey = value => typeof value === 'string' ? value.replace(/\s/gu, '') : null;
  10. export function statisticsRows(section) {
  11. if (section?.id !== STATISTICS_SECTION_ID) return null;
  12. if (section.kind !== 'table' || !Array.isArray(section.data?.cols) || section.data.cols.length !== STATISTICS_COLUMNS.length
  13. || section.data.cols.some((column, index) => columnKey(column) !== columnKey(STATISTICS_COLUMNS[index]))
  14. || !Array.isArray(section.data?.rows) || !section.data.rows.length) invalid();
  15. return section.data.rows.map(row => {
  16. if (!Array.isArray(row) || row.length !== 7) invalid();
  17. const [label, numerator, denominator, scope, interpretation, implication, evidenceId] = row;
  18. if (![label, scope, interpretation, implication].every(hasText) || typeof evidenceId !== 'string'
  19. || !count(numerator) || !count(denominator)
  20. || numerator !== null && denominator !== null && numerator > denominator) invalid();
  21. const ratio = numerator !== null && denominator !== null && denominator > 0 ? numerator / denominator : null;
  22. return { label, numerator, denominator, scope, interpretation, implication, evidenceId,
  23. ratio, percent: ratio === null ? null : (ratio * 100).toFixed(1) + '%' };
  24. });
  25. }
  26. export function validateReportStatistics(report) {
  27. for (const chapter of report?.chapters || []) for (const section of chapter.sections || []) {
  28. const rows = statisticsRows(section);
  29. if (rows?.some(row => row.evidenceId && !Object.hasOwn(report.evidence || {}, row.evidenceId))) invalid();
  30. }
  31. }