Skip to content

Commit 2a1c2a9

Browse files
committed
feat(docs): dual audience authoring and DocsAudienceIntentGate
Add maintainer-docs-site-authoring alongside user-manual-authoring, route docs work by docsAudience x docsSurface with fail-closed ambiguous and drift checks, and cover with npm run test:docs-audience. Update dev-docs handoff, audit-document DA-M, document-sync audience maps, intent expansion, routing, plugin registration, and gate-registry.
1 parent b464c27 commit 2a1c2a9

14 files changed

Lines changed: 479 additions & 24 deletions

File tree

‎changelogs/unreleased.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,7 @@ WorkspaceDataAbsorptionScopeGate · DocsSiteVisualAcceptanceGate · OmissionOnly
123123
- **HomologousDeploy**:`skill-deploy-filter` 同源过滤 gray;作用于 CLI copy 与 `buildDeploymentDescriptors`。
124124
- **V100** + `npm run test:closure-evidence`。
125125
- 需求:`.devcodex/devcodex-v1/requirements/控制面闭合证据与误放行治理/`。
126+
127+
## 2026-07-24
128+
- **[docs-audience]** DocsAudienceIntentGate + user/maintainer site authoring split;
129+
pm run test:docs-audience`n

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"test:visible-output": "node scripts/test-visible-output-contract.js",
6767
"test:external-review-claim": "node scripts/test-external-review-claim-verification.js",
6868
"test:optimization-backlog-evidence": "node scripts/test-optimization-backlog-evidence.js",
69+
"test:docs-audience": "node scripts/test-docs-audience-intent.js",
6970
"test:discipline-execution": "node scripts/test-discipline-execution-probe.js",
7071
"test:host-instruction-projection": "node scripts/generate-host-instruction-projections.js --check && node scripts/test-host-instruction-projection.js",
7172
"test:host-adapters": "node scripts/test-host-adapters.js",

‎plugin.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,11 @@
230230
"file": "skills/user-manual-authoring/SKILL.md",
231231
"tier": "free"
232232
},
233+
{
234+
"id": "maintainer-docs-site-authoring",
235+
"file": "skills/maintainer-docs-site-authoring/SKILL.md",
236+
"tier": "free"
237+
},
233238
{
234239
"id": "review-checklist",
235240
"file": "skills/review-checklist/SKILL.md",
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env node
2+
'use strict'
3+
4+
const assert = require('assert')
5+
const {
6+
classifyDocsAudienceSample,
7+
classifyDocsAudienceDriftSample,
8+
classifyDocsAudienceDisambiguationSample
9+
} = require('./lib/docs-audience-intent')
10+
11+
// Positive: user guide
12+
{
13+
const r = classifyDocsAudienceSample('写开源用户使用文档站,包含安装和快速开始')
14+
assert.strictEqual(r.docsAudience, 'public-user')
15+
assert.strictEqual(r.status, 'ok')
16+
assert.ok(['guide', 'readme'].includes(r.docsSurface) || r.docsSurface === 'guide')
17+
}
18+
19+
// Positive: reference → public-user
20+
{
21+
const r = classifyDocsAudienceSample('写 API 参考文档和 CLI 说明')
22+
assert.strictEqual(r.docsAudience, 'public-user')
23+
assert.strictEqual(r.docsSurface, 'reference')
24+
}
25+
26+
// Positive: maintainer
27+
{
28+
const r = classifyDocsAudienceSample('写维护者开发站点文档,包含 clone、测试与贡献流程')
29+
assert.strictEqual(r.docsAudience, 'maintainer-dev')
30+
assert.strictEqual(r.status, 'ok')
31+
}
32+
33+
// Negative 1: vague website → ambiguous + recommended
34+
{
35+
const r = classifyDocsAudienceSample('把 website 写一下')
36+
assert.strictEqual(r.docsAudience, 'ambiguous')
37+
assert.strictEqual(r.failClosed, true)
38+
assert.strictEqual(r.recommendedAudience, 'public-user')
39+
assert.ok(r.recommendedLabel && /推荐/.test(r.recommendedLabel))
40+
}
41+
42+
// Negative 2: user task body polluted with release checklist first screen
43+
{
44+
const drift = classifyDocsAudienceDriftSample(
45+
'public-user',
46+
'# Release checklist\n\n- tag\n- publish\n\n内部台账状态\n'
47+
)
48+
assert.strictEqual(drift, 'drift-maintainer-on-user')
49+
}
50+
51+
// Negative 3: maintainer body without dev path
52+
{
53+
const drift = classifyDocsAudienceDriftSample(
54+
'maintainer-dev',
55+
'# 产品介绍\n\n这是什么\n适合谁\n产品价值巨大\n'
56+
)
57+
assert.strictEqual(drift, 'drift-no-dev-path')
58+
}
59+
60+
// Multi-audience must split
61+
{
62+
const r = classifyDocsAudienceSample('同时写用户使用文档和维护者贡献指南')
63+
assert.strictEqual(r.docsAudience, 'multi-audience')
64+
assert.strictEqual(r.failClosed, true)
65+
}
66+
67+
// Disambiguation must carry unique recommendation
68+
{
69+
assert.strictEqual(
70+
classifyDocsAudienceDisambiguationSample('受众不明,推荐用户使用站点(推荐),备选维护者开发站'),
71+
'ok'
72+
)
73+
assert.strictEqual(
74+
classifyDocsAudienceDisambiguationSample('你希望哪种?A/B/C 选一个'),
75+
'preference-menu'
76+
)
77+
}
78+
79+
// Healthy user body ok
80+
{
81+
assert.strictEqual(
82+
classifyDocsAudienceDriftSample(
83+
'public-user',
84+
'# 安装\n\nnpm i foo\n\n## 快速开始\n\n第一次运行…\n'
85+
),
86+
'ok'
87+
)
88+
}
89+
90+
// Healthy maintainer body ok
91+
{
92+
assert.strictEqual(
93+
classifyDocsAudienceDriftSample(
94+
'maintainer-dev',
95+
'# 开发\n\ngit clone …\nnpm install\nnpm test\n\n## 贡献\n'
96+
),
97+
'ok'
98+
)
99+
}
100+
101+
console.log('docs-audience-intent tests passed')

‎skills/audit-document/SKILL.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,25 @@ description: 通用文档审查维度 DA-1~DA-6 — README/架构文档/开发
5959
4. 通用结构/准确性问题留在 `DA-*`
6060
5. 用户路径、快速开始、示例真实度、开发信息后置与消费链一致性问题留在 `RM-*`
6161

62+
## 维护者站点维(DA-M · 条件)
63+
64+
当审查目标为**维护者/贡献者开发文档站**(`docsAudience=maintainer-dev` 或 CONTRIBUTING/dev/internals 主路径)时,在 DA-1~DA-6 之上追加:
65+
66+
| 检查 | 要求 |
67+
|------|------|
68+
| DA-M1 可执行 dev 路径 | 具备 clone/环境/依赖安装与 test 或 build 命令之一;不得仅产品介绍 |
69+
| DA-M2 受众一致 | 主叙事服务维护者;不得伪装成用户安装手册却无用户第一次成功路径 |
70+
| DA-M3 命令可信 | 关键命令与 `package.json`/仓库脚本不矛盾,或标明 N/A 理由 |
71+
| DA-M4 漂移 | `classifyDocsAudienceDriftSample('maintainer-dev', body)` 不得为 `drift-no-dev-path` |
72+
73+
用户站审查仍优先 `audit-user-manual`;本维不替代用户站聚合入口。
74+
6275
## N/A 规则
6376

6477
- 纯图表/示意图文件无受众概念:DA-5 标 N/A
6578
- 无关联代码:DA-6 标 N/A
6679
- 未触发用户手册 / 文档站 / 多语言 / 生成站点 / 示例语义 / 专家产物时:对应条件门禁聚合写 `N/A + skipReason`(完整 Gate 名见 registry / Owner Skill)
80+
- 未触发维护者站:DA-M 聚合 `N/A + skipReason=not-maintainer-docs`
6781
- 公开主路径若展示旧兼容路径或 DSL/parser 示例:分别检查副作用兼容边界与最小执行探针(Owner:`user-manual-authoring` / `dev-docs`)
6882

6983

‎skills/dev-docs/SKILL.md‎

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,27 @@ description: 文档开发子类型规范 — 技术文档/API文档/README 编
66

77
## 触发条件
88

9-
用户要求编写/更新文档:README、站点文档、用户使用文档、API 文档、架构文档、开发指南、CHANGELOG、迁移指南等。
9+
用户要求编写/更新**技术类**文档:API/契约说明、架构文档、开发指南、迁移**实现**说明、通用技术 Markdown 等。
10+
11+
> ⛔ **入口分流(DocsAudienceIntent,强制)**
12+
> 写文档任务须先判定 `docsAudience` + `docsSurface`(`scripts/lib/docs-audience-intent.js` / registry `docs-audience-intent`):
13+
> - `public-user`(用户使用站 / README / 用户手册 / 用户向 changelog·operations·reference)→ **必须 handoff** `user-manual-authoring`(+ 条件 `readme-authoring`),**不得**以本 Skill 为主写作入口。
14+
> - `maintainer-dev`(维护者开发站 / contributing / 发版 runbook / ADR 站)→ **必须 handoff** `maintainer-docs-site-authoring`。
15+
> - `ambiguous` / `multi-audience` → **阻断**;ambiguous 须唯一推荐消歧;multi 须拆任务。
16+
> - 仅当受众已是技术读者且 surface 为契约/架构/通用技术文时,本 Skill 才作为主入口(light-api / frontend-api / general-doc)。
1017
1118
## 豁免项
1219

13-
- 豁免 `plan-review`(文档任务不需要实施计划审查)
20+
- 豁免 `plan-review`(**业务文档内容**任务不需要实施计划审查)
1421
- 豁免 `impact-review`(文档变更不涉及代码影响评估)
15-
- 豁免 CP3(无需实施计划);必须记录 `CP3: N/A(docs 子类型豁免)`,供 hook/fallback 区分合法豁免与漏确认
16-
- CP2 简化为**文档大纲确认**(不需要完整技术方案)
22+
- 豁免 CP3(**仅** `dev.docs` 子类型写业务项目文档时);必须记录 `CP3: N/A(docs 子类型豁免)`
23+
- ⚠️ **控制面 / Skill / 规范包变更**(如本仓库改 skills)走 `dev.default`,**不享受**本豁免
24+
- CP2 简化为**文档大纲确认**(业务文档内容任务;控制面任务仍完整技术方案)
1725

1826
## 目标文档分流
1927

20-
当任务属于“契约驱动型文档”时,优先先冻结目标文档,再让后续实现或联动产物围绕它落地。
28+
当任务属于“契约驱动型文档”且已锁定为技术契约(非用户站 narrative)时,优先先冻结目标文档,再让后续实现或联动产物围绕它落地。
29+
用户侧 **reference** 可由 `user-manual-authoring` **编排** 本 Skill 的 light-api,但 Owner 仍是用户站。
2130

2231
### 何时视为契约驱动型文档
2332

@@ -35,16 +44,15 @@ description: 文档开发子类型规范 — 技术文档/API文档/README 编
3544
| `frontend-api` | 前端联调、页面/模块接口说明、字段映射说明 | Markdown 前端接口文档 |
3645
| `general-doc` | 架构文档、开发指南、迁移指南、治理说明、运行手册 | Markdown 通用文档 |
3746

38-
## README 专项写作分支
39-
40-
当目标文档是站点文档、用户使用文档、最终用户使用文档、最终用户手册、README、quick start、接入手册或公开能力页时,优先调用 `user-manual-authoring`,先冻结用户主路径、文档落点和信息架构,再决定是否进入 README 专项分支。
47+
## README / 用户站 / 维护者站 handoff(强制)
4148

42-
当目标文档是 `README.md` 或承担主使用入口职责的 README / 项目主文档时,在 `user-manual-authoring` 基础上继续调用 `readme-authoring`:
49+
| 目标 | handoff |
50+
|------|---------|
51+
| 用户站、用户手册、README、quick start、接入手册、公开能力页 | **`user-manual-authoring`**(+ 条件 `readme-authoring`) |
52+
| 维护者开发站、CONTRIBUTING 站区、发版 runbook 主叙事 | **`maintainer-docs-site-authoring`** |
53+
| light-api / frontend-api / 架构 general-doc(技术读者) | 本 Skill 主入口 |
4354

44-
- 默认第一受众是**用户 / 使用者**
45-
- 快速开始、常见用法、配置与排错必须早于开发/贡献内容
46-
- 章节骨架优先使用 `prompts/project-readme.prompt.md`
47-
- 完成后若需要用户侧文档 review、项目文档审查、菜单导航或信息架构审查,优先叠加 `audit-user-manual`;落点为 README / 主入口文档时再叠加 `audit-readme`
55+
README 专项仍由 `user-manual-authoring` + `readme-authoring` 承接(默认受众=使用者;开发/贡献后置)。
4856

4957
## 文档质量标准
5058

0 commit comments

Comments
 (0)