Skip to content

Commit 1649163

Browse files
committed
feat(docs): add surface inventory gate and evidence matrix for maintainer site
Add docs-surface-inventory probe, validation-manifest nodes, expanded public-text surfaces, and intro capability evidence table (declare/probe/live). Wire into control-plane tests. Closes Fix-0 of maintainer site consistency.
1 parent 87c6c14 commit 1649163

8 files changed

Lines changed: 388 additions & 4 deletions

File tree

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ coverage/
2323
.tmp-hook-editfiles.json
2424
.tmp-hook-terminal.json
2525

26+
# Local analysis / inventory scratch (never commit)
27+
.tmp/
28+
.tmp/**
29+
2630
source-root-deployment-backup-*/
2731
tmp-validate*.txt
2832
package.json.tmp-test

‎changelogs/unreleased.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
66
## 当前未发布实现候选
77

8+
- **维护者站点×完整功能清单对账门(docs-surface-inventory)**:新增 `scripts/lib/docs-surface-inventory.js` + `npm run test:docs-surface-inventory`;校验 Skills84/plugin 双向、workflows×8(含 other)、MCP15、Hooks5/runtime、prompts30、instructions15、website≥156、process-enforcement 文件与 script;挂入 `test:control-plane` 与 validation-manifest 节点 `docs-surface-inventory` / `process-enforcement-e2e`(fast/full)。public-text-surfaces 扩展 guide/index 与 host-parity-grok。intro 增加能力证据三列表。
89
- **全宿主流程强制 + 产物路径准确(process-enforcement)**:新增 `scripts/lib/process-enforcement.js`(MutationCpGate 受保护路径 hard-deny、ArtifactPathGate、复审清单/过程包齐套判定)与 `host-enforcement-matrix.js`(五宿主策略真相源);`lifecycle.cjs` PreTool 接线;`lifecycle-stop-gate.cjs` 追加 `review-checklist-missing` / `process-artifact-incomplete` / `progress-artifact-missing`;`npm run test:process-enforcement-e2e` 并入 `test:stop-gate` 与 `test:control-plane`。文档:`02-output-paths` ArtifactPathGate 行、README 流程强制条;配套 PI-REQ-ASSESS 需求模板评估字段。
910
- **需求模板强制项目实况评估(PI-REQ-ASSESS-20260726)**:`requirement-overview.prompt.md` 新增 §10(AI 填写:CodeTruth 锚点 + 合理性/可实施性/收益/验证状态/影响范围 + 推荐);`requirement.prompt.md` 新增 §2.6 同构字段;与 compliance 五项验证对齐;禁止空表过关。台账 PI-REQ-ASSESS / PI-REQ-NO-SPLIT。需求:`全宿主流程强制与产物路径准确` 概况 v0.4。
1011
- **Stop 审查修复批(F-01~F-16)**:`lastAssistantMessage` 进入 visible 证据;完成检查识别 `### DevCodex · 完成检查`;PR-1 强证据;R11 gap 命名 + `completion-check-missing`;report/memory 可 N/A 豁免;host-parity 站点条件硬续文案;R10 路径/Honesty 探针;hook 矩阵优先源码 + adapter 保留 block。验证:`npm run test:stop-gate`。
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
'use strict'
2+
3+
/**
4+
* Docs × public-surface inventory scanner (live disk truth).
5+
* Used by test-docs-surface-inventory and site-consistency gates.
6+
*/
7+
8+
const fs = require('fs')
9+
const path = require('path')
10+
11+
const REQUIRED_WORKFLOWS = Object.freeze([
12+
'analyze',
13+
'audit',
14+
'chat',
15+
'dev',
16+
'fix',
17+
'other',
18+
'resume',
19+
'self-fix'
20+
])
21+
22+
const REQUIRED_HOOK_EVENTS = Object.freeze([
23+
'UserPromptSubmit',
24+
'PreToolUse',
25+
'PostToolUse',
26+
'PreCompact',
27+
'Stop'
28+
])
29+
30+
const REQUIRED_PROCESS_FILES = Object.freeze([
31+
'scripts/lib/process-enforcement.js',
32+
'scripts/lib/host-enforcement-matrix.js',
33+
'scripts/test-process-enforcement-e2e.js'
34+
])
35+
36+
function listDirs (root, dir) {
37+
const full = path.join(root, dir)
38+
if (!fs.existsSync(full)) return []
39+
return fs
40+
.readdirSync(full, { withFileTypes: true })
41+
.filter((e) => e.isDirectory() && !e.name.startsWith('_') && !e.name.startsWith('.'))
42+
.map((e) => e.name)
43+
.sort()
44+
}
45+
46+
function listFiles (root, dir, re) {
47+
const full = path.join(root, dir)
48+
if (!fs.existsSync(full)) return []
49+
return fs
50+
.readdirSync(full)
51+
.filter((f) => re.test(f))
52+
.sort()
53+
}
54+
55+
function walkMd (root, relDir, acc = []) {
56+
const full = path.join(root, relDir)
57+
if (!fs.existsSync(full)) return acc
58+
for (const e of fs.readdirSync(full, { withFileTypes: true })) {
59+
const r = path.join(relDir, e.name).replace(/\\/g, '/')
60+
const p = path.join(full, e.name)
61+
if (e.isDirectory()) walkMd(root, r, acc)
62+
else if (/\.mdx?$/i.test(e.name)) acc.push(r)
63+
}
64+
return acc
65+
}
66+
67+
function extractMcpToolNames (root, file) {
68+
const p = path.join(root, file)
69+
if (!fs.existsSync(p)) return []
70+
const text = fs.readFileSync(p, 'utf8')
71+
const names = new Set()
72+
const re = /name:\s*'([a-z][a-z0-9_]*)'/g
73+
let m
74+
while ((m = re.exec(text))) {
75+
if (m[1].startsWith('memory_') || m[1].startsWith('profile_')) names.add(m[1])
76+
}
77+
return [...names].sort()
78+
}
79+
80+
/**
81+
* @param {string} root package root
82+
* @returns {object} live surface inventory
83+
*/
84+
function scanDocsSurfaceInventory (root) {
85+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
86+
const plugin = JSON.parse(fs.readFileSync(path.join(root, 'plugin.json'), 'utf8'))
87+
const pluginSkillIds = (plugin.skills || [])
88+
.map((s) => (typeof s === 'string' ? s : s.id))
89+
.filter(Boolean)
90+
.sort()
91+
const skillDirs = listDirs(root, 'skills')
92+
const skillWithMd = skillDirs.filter((d) =>
93+
fs.existsSync(path.join(root, 'skills', d, 'SKILL.md'))
94+
)
95+
96+
const wf = JSON.parse(
97+
fs.readFileSync(path.join(root, 'skills/routing/workflow-capabilities.json'), 'utf8')
98+
)
99+
const workflowIds = (Array.isArray(wf.workflows) ? wf.workflows : [])
100+
.map((w) => w.id)
101+
.filter(Boolean)
102+
.sort()
103+
104+
const life = JSON.parse(fs.readFileSync(path.join(root, 'hooks/devcodex.lifecycle.json'), 'utf8'))
105+
const hookEvents = Object.keys(life.hooks || {}).sort()
106+
107+
const hookRuntime = listFiles(root, 'hooks/_runtime', /\.cjs$/i)
108+
const prompts = listFiles(root, 'prompts', /\.prompt\.md$/i)
109+
const instructionsMain = listFiles(root, 'instructions', /\.instructions\.md$/i)
110+
const scriptsLib = listFiles(root, 'scripts/lib', /\.js$/i)
111+
const scriptsTop = listFiles(root, 'scripts', /\.(js|cjs|mjs)$/i)
112+
const npmScripts = Object.keys(pkg.scripts || {}).sort()
113+
const websiteMd = walkMd(root, 'website/docs').sort()
114+
115+
const gates = JSON.parse(
116+
fs.readFileSync(path.join(root, 'skills/spec-governance/gate-registry.json'), 'utf8')
117+
)
118+
let gateGroups = 0
119+
if (Array.isArray(gates.groups)) gateGroups = gates.groups.length
120+
else if (gates.groups && typeof gates.groups === 'object') gateGroups = Object.keys(gates.groups).length
121+
else if (gates.gateGroups) gateGroups = Object.keys(gates.gateGroups).length
122+
123+
const manifest = JSON.parse(
124+
fs.readFileSync(path.join(root, 'scripts/validation-manifest.json'), 'utf8')
125+
)
126+
const validationNodes = (manifest.nodes || []).map((n) => n.id).filter(Boolean)
127+
128+
const memoryTools = extractMcpToolNames(root, 'mcp/memory-server.js')
129+
const profileTools = extractMcpToolNames(root, 'mcp/profile-server.js')
130+
131+
const processFiles = {}
132+
for (const f of REQUIRED_PROCESS_FILES) {
133+
processFiles[f] = fs.existsSync(path.join(root, f))
134+
}
135+
136+
return {
137+
schemaVersion: 'DocsSurfaceInventoryV1',
138+
skillDirs: skillDirs.length,
139+
skillWithMd: skillWithMd.length,
140+
pluginSkillIds: pluginSkillIds.length,
141+
skillOnlyPlugin: pluginSkillIds.filter((id) => !skillDirs.includes(id)),
142+
skillOnlyDisk: skillDirs.filter((id) => !pluginSkillIds.includes(id)),
143+
workflowIds,
144+
workflowCount: workflowIds.length,
145+
mcpTools: [...memoryTools, ...profileTools],
146+
mcpToolCount: memoryTools.length + profileTools.length,
147+
hookEvents,
148+
hookRuntimeCount: hookRuntime.length,
149+
prompts: prompts.length,
150+
instructionsMain: instructionsMain.length,
151+
scriptsLib: scriptsLib.length,
152+
scriptsTop: scriptsTop.length,
153+
npmScripts: npmScripts.length,
154+
websiteMd: websiteMd.length,
155+
gateGroups,
156+
validationNodes: validationNodes.length,
157+
processFiles,
158+
hasProcessEnforcementScript: Boolean(pkg.scripts && pkg.scripts['test:process-enforcement-e2e'])
159+
}
160+
}
161+
162+
/**
163+
* @param {object} inv
164+
* @returns {{ ok: boolean, failures: string[] }}
165+
*/
166+
function assertDocsSurfaceInventory (inv) {
167+
const failures = []
168+
169+
if (inv.skillDirs !== 84) failures.push(`skills dirs expected 84 got ${inv.skillDirs}`)
170+
if (inv.pluginSkillIds !== 84) failures.push(`plugin skills expected 84 got ${inv.pluginSkillIds}`)
171+
if (inv.skillWithMd !== inv.skillDirs) {
172+
failures.push(`SKILL.md missing for some skill dirs (${inv.skillWithMd}/${inv.skillDirs})`)
173+
}
174+
if (inv.skillOnlyPlugin.length) {
175+
failures.push(`plugin-only skills: ${inv.skillOnlyPlugin.join(',')}`)
176+
}
177+
if (inv.skillOnlyDisk.length) {
178+
failures.push(`disk-only skills: ${inv.skillOnlyDisk.join(',')}`)
179+
}
180+
181+
const wfSorted = [...inv.workflowIds].sort()
182+
const reqSorted = [...REQUIRED_WORKFLOWS].sort()
183+
if (JSON.stringify(wfSorted) !== JSON.stringify(reqSorted)) {
184+
failures.push(`workflows mismatch got [${wfSorted.join(',')}]`)
185+
}
186+
if (inv.workflowIds.includes('plan')) {
187+
failures.push('workflow id "plan" must not exist (use other + plan Skill)')
188+
}
189+
190+
if (inv.mcpToolCount !== 15) failures.push(`mcp tools expected 15 got ${inv.mcpToolCount}`)
191+
192+
for (const ev of REQUIRED_HOOK_EVENTS) {
193+
if (!inv.hookEvents.includes(ev)) failures.push(`missing hook event ${ev}`)
194+
}
195+
if (inv.hookRuntimeCount < 26) {
196+
failures.push(`hook runtime cjs expected >=26 got ${inv.hookRuntimeCount}`)
197+
}
198+
199+
if (inv.prompts !== 30) failures.push(`prompts expected 30 got ${inv.prompts}`)
200+
if (inv.instructionsMain !== 15) {
201+
failures.push(`instructions main expected 15 got ${inv.instructionsMain}`)
202+
}
203+
if (inv.websiteMd < 156) failures.push(`website md expected >=156 got ${inv.websiteMd}`)
204+
if (inv.gateGroups !== 51) failures.push(`gate groups expected 51 got ${inv.gateGroups}`)
205+
if (inv.validationNodes < 83) {
206+
failures.push(`validation nodes expected >=83 got ${inv.validationNodes}`)
207+
}
208+
if (inv.npmScripts < 109) failures.push(`npm scripts expected >=109 got ${inv.npmScripts}`)
209+
if (inv.scriptsLib < 102) failures.push(`scripts/lib expected >=102 got ${inv.scriptsLib}`)
210+
211+
for (const [f, ok] of Object.entries(inv.processFiles || {})) {
212+
if (!ok) failures.push(`missing required file ${f}`)
213+
}
214+
if (!inv.hasProcessEnforcementScript) {
215+
failures.push('package.json missing script test:process-enforcement-e2e')
216+
}
217+
218+
return { ok: failures.length === 0, failures }
219+
}
220+
221+
module.exports = {
222+
REQUIRED_WORKFLOWS,
223+
REQUIRED_HOOK_EVENTS,
224+
REQUIRED_PROCESS_FILES,
225+
scanDocsSurfaceInventory,
226+
assertDocsSurfaceInventory
227+
}

‎scripts/public-text-surfaces.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
{ "path": "README.md", "role": "package-readme" },
55
{ "path": "changelogs/unreleased.md", "role": "current-changelog" },
66
{ "path": "website/docs/intro/index.md", "role": "website-home" },
7+
{ "path": "website/docs/guide/index.md", "role": "website-guide-index" },
78
{ "path": "website/docs/guide/development.md", "role": "website-current-guide" },
8-
{ "path": "website/docs/specs/directory-structure.md", "role": "website-current-spec" }
9+
{ "path": "website/docs/specs/directory-structure.md", "role": "website-current-spec" },
10+
{ "path": "website/docs/intro/host-parity-grok.md", "role": "website-host-parity-grok" }
911
],
1012
"roots": [
1113
{ "path": "instructions", "role": "normative-instructions", "extensions": [".md"] },
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env node
2+
'use strict'
3+
4+
/**
5+
* Live docs/public-surface inventory gate (Fix-0 / V-GAP-01/02).
6+
*/
7+
8+
const path = require('path')
9+
const {
10+
scanDocsSurfaceInventory,
11+
assertDocsSurfaceInventory
12+
} = require('./lib/docs-surface-inventory.js')
13+
14+
const ROOT = path.resolve(__dirname, '..')
15+
const inv = scanDocsSurfaceInventory(ROOT)
16+
const result = assertDocsSurfaceInventory(inv)
17+
18+
if (!result.ok) {
19+
console.error('docs-surface-inventory FAILED:')
20+
for (const f of result.failures) console.error(' -', f)
21+
console.error(JSON.stringify({ skillDirs: inv.skillDirs, workflows: inv.workflowIds, mcp: inv.mcpToolCount }, null, 2))
22+
process.exit(1)
23+
}
24+
25+
console.log('docs-surface-inventory PASS')
26+
console.log(
27+
JSON.stringify(
28+
{
29+
skills: inv.skillDirs,
30+
workflows: inv.workflowCount,
31+
mcpTools: inv.mcpToolCount,
32+
hooks: inv.hookEvents.length,
33+
hookRuntime: inv.hookRuntimeCount,
34+
prompts: inv.prompts,
35+
instructions: inv.instructionsMain,
36+
websiteMd: inv.websiteMd,
37+
npmScripts: inv.npmScripts,
38+
scriptsLib: inv.scriptsLib,
39+
validationNodes: inv.validationNodes,
40+
gateGroups: inv.gateGroups,
41+
processFiles: inv.processFiles
42+
},
43+
null,
44+
2
45+
)
46+
)

0 commit comments

Comments
 (0)