|
| 1 | +'use strict' |
| 2 | + |
| 3 | +/** |
| 4 | + * DocsAudienceIntentGate classifiers (CP1/CP2: 文档受众双 Skill 分流). |
| 5 | + * Portable pure functions — no I/O. |
| 6 | + */ |
| 7 | + |
| 8 | +/** |
| 9 | + * @typedef {'public-user'|'maintainer-dev'|'ambiguous'|'multi-audience'} DocsAudience |
| 10 | + * @typedef {'guide'|'readme'|'reference'|'migration'|'changelog'|'operations'|'maintainer'|'other'} DocsSurface |
| 11 | + */ |
| 12 | + |
| 13 | +/** |
| 14 | + * @param {string} prompt |
| 15 | + * @param {{ pathHints?: string[] }} [opts] |
| 16 | + * @returns {{ |
| 17 | + * docsAudience: DocsAudience, |
| 18 | + * docsSurface: DocsSurface, |
| 19 | + * recommendedAudience: 'public-user'|'maintainer-dev'|null, |
| 20 | + * recommendedLabel: string|null, |
| 21 | + * signals: string[], |
| 22 | + * status: 'ok'|'ambiguous'|'multi-audience', |
| 23 | + * failClosed: boolean |
| 24 | + * }} |
| 25 | + */ |
| 26 | +function classifyDocsAudienceSample(prompt, opts = {}) { |
| 27 | + const text = String(prompt || '') |
| 28 | + const pathBlob = (opts.pathHints || []).map(String).join(' ') |
| 29 | + const blob = `${text}\n${pathBlob}` |
| 30 | + const signals = [] |
| 31 | + |
| 32 | + const hasUserPhrase = /用户使用|使用文档|安装|quick\s*start|快速开始|接入|给使用者|开源用户|第一次成功|how to use|getting started/i.test(blob) |
| 33 | + const hasMaintPhrase = /维护者|贡献|contributing|本地开发|clone|发版\s*runbook|release\s*checklist|internals|ADR|开发站点|给维护/i.test(blob) |
| 34 | + const hasReadme = /\bREADME\b|主入口文档/i.test(blob) |
| 35 | + const hasReference = /\bAPI\b|CLI|Config\s*参考|接口参考|reference/i.test(blob) |
| 36 | + const hasMigrationUser = /升级指南|迁移指南|从\s*v?\d|upgrade\s+guide/i.test(blob) && !/实现迁移|迁移代码|写迁移/i.test(blob) |
| 37 | + const hasMigrationImpl = /实现迁移|迁移代码|写迁移|migration\s+impl/i.test(blob) |
| 38 | + const hasChangelogUser = /changelog|更新日志|release\s*notes/i.test(blob) && !/发版\s*runbook|发布清单|release\s*checklist/i.test(blob) |
| 39 | + const hasChangelogMaint = /发版\s*runbook|发布清单|release\s*checklist|tag\s*发布流程/i.test(blob) |
| 40 | + const hasOpsUser = /自托管|部署方|运维手册|operations/i.test(blob) && !/值班|on-?call|内部\s*runbook/i.test(blob) |
| 41 | + const hasOpsMaint = /值班|on-?call|内部\s*runbook/i.test(blob) |
| 42 | + const vagueSiteOnly = /^(?:请)?(?:把|将)?(?:website|文档站|站点文档)(?:写一下|补全|更新)?[.。!!]?$/i.test(text.trim()) || |
| 43 | + (/写(?:一下)?(?:website|文档站|站点文档)/i.test(text) && !hasUserPhrase && !hasMaintPhrase && !hasReadme && !hasReference) |
| 44 | + |
| 45 | + const pathUser = /\/(guide|docs\/intro|getting-started|quick-start)\//i.test(pathBlob) |
| 46 | + const pathMaint = /\/(contributing|internals|development|maintainer)\//i.test(pathBlob) |
| 47 | + |
| 48 | + if (hasUserPhrase) signals.push('phrase:public-user') |
| 49 | + if (hasMaintPhrase) signals.push('phrase:maintainer-dev') |
| 50 | + if (hasReadme) signals.push('phrase:readme') |
| 51 | + if (hasReference) signals.push('phrase:reference') |
| 52 | + if (vagueSiteOnly) signals.push('phrase:vague-site') |
| 53 | + if (pathUser) signals.push('path:user') |
| 54 | + if (pathMaint) signals.push('path:maintainer') |
| 55 | + |
| 56 | + const userHit = hasUserPhrase || hasReadme || hasReference || hasMigrationUser || hasChangelogUser || hasOpsUser || pathUser |
| 57 | + const maintHit = hasMaintPhrase || hasMigrationImpl || hasChangelogMaint || hasOpsMaint || pathMaint |
| 58 | + |
| 59 | + if (userHit && maintHit) { |
| 60 | + return { |
| 61 | + docsAudience: 'multi-audience', |
| 62 | + docsSurface: 'other', |
| 63 | + recommendedAudience: null, |
| 64 | + recommendedLabel: null, |
| 65 | + signals, |
| 66 | + status: 'multi-audience', |
| 67 | + failClosed: true |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + if (vagueSiteOnly || (!userHit && !maintHit && /文档站|website|站点文档|写文档/i.test(text))) { |
| 72 | + const recommendedAudience = pathMaint ? 'maintainer-dev' : (pathUser || hasReadme ? 'public-user' : 'public-user') |
| 73 | + // path-only vague: still ambiguous if no path; with path can soft-recommend |
| 74 | + const pureVague = vagueSiteOnly || (!pathUser && !pathMaint && !hasReadme) |
| 75 | + if (pureVague) { |
| 76 | + return { |
| 77 | + docsAudience: 'ambiguous', |
| 78 | + docsSurface: 'other', |
| 79 | + recommendedAudience: 'public-user', |
| 80 | + recommendedLabel: '用户使用站点(安装/接入/排错)(推荐)', |
| 81 | + signals: signals.length ? signals : ['phrase:vague-docs'], |
| 82 | + status: 'ambiguous', |
| 83 | + failClosed: true |
| 84 | + } |
| 85 | + } |
| 86 | + } |
| 87 | + |
| 88 | + if (maintHit) { |
| 89 | + let surface = 'maintainer' |
| 90 | + if (hasMigrationImpl) surface = 'maintainer' |
| 91 | + if (hasChangelogMaint) surface = 'maintainer' |
| 92 | + return { |
| 93 | + docsAudience: 'maintainer-dev', |
| 94 | + docsSurface: surface, |
| 95 | + recommendedAudience: null, |
| 96 | + recommendedLabel: null, |
| 97 | + signals, |
| 98 | + status: 'ok', |
| 99 | + failClosed: false |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + if (userHit) { |
| 104 | + let surface = 'guide' |
| 105 | + if (hasReadme) surface = 'readme' |
| 106 | + if (hasReference) surface = 'reference' |
| 107 | + if (hasMigrationUser) surface = 'migration' |
| 108 | + if (hasChangelogUser) surface = 'changelog' |
| 109 | + if (hasOpsUser) surface = 'operations' |
| 110 | + return { |
| 111 | + docsAudience: 'public-user', |
| 112 | + docsSurface: surface, |
| 113 | + recommendedAudience: null, |
| 114 | + recommendedLabel: null, |
| 115 | + signals, |
| 116 | + status: 'ok', |
| 117 | + failClosed: false |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + return { |
| 122 | + docsAudience: 'ambiguous', |
| 123 | + docsSurface: 'other', |
| 124 | + recommendedAudience: 'public-user', |
| 125 | + recommendedLabel: '用户使用站点(安装/接入/排错)(推荐)', |
| 126 | + signals: signals.length ? signals : ['none'], |
| 127 | + status: 'ambiguous', |
| 128 | + failClosed: true |
| 129 | + } |
| 130 | +} |
| 131 | + |
| 132 | +/** |
| 133 | + * Detect audience drift in drafted doc body for a locked audience. |
| 134 | + * @param {'public-user'|'maintainer-dev'} audience |
| 135 | + * @param {string} body |
| 136 | + * @returns {'ok'|'drift-maintainer-on-user'|'drift-no-dev-path'|'not-applicable'} |
| 137 | + */ |
| 138 | +function classifyDocsAudienceDriftSample(audience, body) { |
| 139 | + const text = String(body || '') |
| 140 | + if (!text.trim()) return 'not-applicable' |
| 141 | + |
| 142 | + if (audience === 'public-user') { |
| 143 | + const maintainerPollution = /release\s*checklist|发版清单|monorepo\s*架构|内部台账|ADR\s*列表|contributing\s*流程(?!.*安装)/i.test(text) |
| 144 | + const hasUserPath = /安装|install|快速开始|quick\s*start|第一次|npm\s+i|pnpm\s+add|yarn\s+add|使用/i.test(text) |
| 145 | + if (maintainerPollution && !hasUserPath) return 'drift-maintainer-on-user' |
| 146 | + if (maintainerPollution && /^(?:#|\s)*release\s*checklist/im.test(text.slice(0, 400))) return 'drift-maintainer-on-user' |
| 147 | + return 'ok' |
| 148 | + } |
| 149 | + |
| 150 | + if (audience === 'maintainer-dev') { |
| 151 | + const hasDevPath = /clone|git\s+clone|npm\s+(?:i|install|test|run)|pnpm|yarn|本地开发|贡献|contributing|环境/i.test(text) |
| 152 | + const onlyProduct = /这是什么|适合谁|产品价值/.test(text) && !hasDevPath |
| 153 | + if (onlyProduct || !hasDevPath) return 'drift-no-dev-path' |
| 154 | + return 'ok' |
| 155 | + } |
| 156 | + |
| 157 | + return 'not-applicable' |
| 158 | +} |
| 159 | + |
| 160 | +/** |
| 161 | + * @param {string} disambiguationText assistant text when status=ambiguous |
| 162 | + * @returns {'ok'|'missing-recommendation'|'preference-menu'} |
| 163 | + */ |
| 164 | +function classifyDocsAudienceDisambiguationSample(disambiguationText) { |
| 165 | + const text = String(disambiguationText || '') |
| 166 | + const hasRecommended = /推荐|(推荐)|\(推荐\)|recommended/i.test(text) |
| 167 | + const flatMenu = /你希望哪种|选一个|A\s*\/\s*B\s*\/\s*C|A\/B\/C|which would you prefer|pick one of/i.test(text) && !hasRecommended |
| 168 | + if (flatMenu) return 'preference-menu' |
| 169 | + if (!/ambiguous|消歧|受众|public-user|maintainer|用户|维护者|推荐/i.test(text)) { |
| 170 | + return 'missing-recommendation' |
| 171 | + } |
| 172 | + if (!hasRecommended) return 'missing-recommendation' |
| 173 | + return 'ok' |
| 174 | +} |
| 175 | + |
| 176 | +module.exports = { |
| 177 | + classifyDocsAudienceSample, |
| 178 | + classifyDocsAudienceDriftSample, |
| 179 | + classifyDocsAudienceDisambiguationSample |
| 180 | +} |
0 commit comments