Skip to content

Commit bc54841

Browse files
committed
fix: correct release CI scope and Skill counts
1 parent 3047606 commit bc54841

11 files changed

Lines changed: 63 additions & 24 deletions

‎changelogs/releases/v1.20.4.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
- 普通和最小压缩投影保留经校验的 taskless pending ingress 与有界 Stop 决策,使真实新入口可以继续准入。
1414
- 自动检查点在没有会话环境变量时复用服务器已观察的 ContextRead/回执身份;显式环境身份仍优先,两个服务器来源冲突时拒绝续租和 CP 写入。
1515
- 新候选允许保留能与成功旧发行按同一提交对账的历史失败 CI;新候选自己的 CI 与最终已发布状态仍单独严格验证,不要求改写历史事实。
16+
- 同步 V92 校验器和公开分类说明中的 Skill 数量;源码 CI 明确区分未部署的工作区 Profile,已有工作区继续严格校验。
17+
- 更新两个 brace-expansion 开发依赖锁节点,修复已公告的拒绝服务漏洞;MCP 回归超时输出具体请求阶段和进程诊断。
1618

1719
## 兼容与验证边界
1820

‎content/skills/portfolio.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
"sidecarDigest": "774105d7947b340d15d17115fcb8e37b62ae545cf553f06126d529184fd74872",
1111
"pluginDigest": "7b4042b10fc27cb70440fef64151bc50662a2f871274749925ea0e88f38388f4",
1212
"publicTaxonomy": "content/skills/public-taxonomy.json",
13-
"publicTaxonomyDigest": "d8f3a3e9728cd04929750e8858e100fd60a4a0afc095b0217ac0877b02f0a7f2",
13+
"publicTaxonomyDigest": "7f6ee8c30f5484b8881e27099af72c69625b73a7cf3b53fe51c8c0c147d4f124",
1414
"portfolioEvidence": "content/skills/portfolio-evidence.json",
1515
"portfolioEvidenceDigest": "fddc547986599fe20672364d6432633edabeb60ee6c5f3da5f9ca083daed6e1e",
1616
"consumerInventoryFileCount": 722,
1717
"consumerInventoryDigest": "915e197c2e882223da7e45eb7f12b6c07054fc461945ae123e363170c489ec3e",
1818
"consumerProjectionDigest": "09e046eaea5934acc8a041c61c4b69dd9bc2e43e062395832a1d438bcc33bc28",
19-
"portfolioInputDigest": "0413ef6be47fd178f3de8b9700d27f95ea0621ff335e6f63eeeef28e48a9d0fa"
19+
"portfolioInputDigest": "e6c6fa9855f782f044a8c1f808ab6a186bbaf11e942bd17812e105dc13f4aeb3"
2020
},
2121
"ordering": "skills.id asc; graph edges from/to asc",
2222
"summary": {
@@ -49,7 +49,7 @@
4949
"assignmentScope": "bundled-registered-only",
5050
"includedInAssignments": false,
5151
"includedInBundledCounts": false,
52-
"description": "Workspace Skills are project-scoped extensions. They are explained separately and never enter bundled Skill assignments or the 86/83/3 denominators."
52+
"description": "Workspace Skills are project-scoped extensions. They are explained separately and never enter bundled Skill assignments or the 87/84/3 denominators."
5353
},
5454
"categories": [
5555
{

‎content/skills/public-taxonomy.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"assignmentScope": "bundled-registered-only",
88
"includedInAssignments": false,
99
"includedInBundledCounts": false,
10-
"description": "Workspace Skills are project-scoped extensions. They are explained separately and never enter bundled Skill assignments or the 86/83/3 denominators."
10+
"description": "Workspace Skills are project-scoped extensions. They are explained separately and never enter bundled Skill assignments or the 87/84/3 denominators."
1111
},
1212
"categories": [
1313
{

‎package-lock.json‎

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎public-site/data/public-product-projection.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"schemaVersion": "PublicProductProjectionV2",
3-
"generatedAt": "2026-10-10T11:31:11.818Z",
3+
"generatedAt": "2026-10-10T16:48:17.854Z",
44
"release": {
55
"version": "1.20.4"
66
},
@@ -1089,7 +1089,7 @@
10891089
"assignmentScope": "bundled-registered-only",
10901090
"includedInAssignments": false,
10911091
"includedInBundledCounts": false,
1092-
"description": "Workspace Skills are project-scoped extensions. They are explained separately and never enter bundled Skill assignments or the 86/83/3 denominators."
1092+
"description": "Workspace Skills are project-scoped extensions. They are explained separately and never enter bundled Skill assignments or the 87/84/3 denominators."
10931093
}
10941094
},
10951095
"capabilityScenarios": [
@@ -1194,7 +1194,7 @@
11941194
"package": "300515260d15b4b90a1c05f110170ad2b54a367762f3b80da7cbdd94a82776b8",
11951195
"expression": "60e525d34e5531e6b4bbbb77f8b34bb93c9ca9ebee5df4fe2812749232175c3c",
11961196
"workflows": "d9eb7086866fe3c3815f673957ed978c1594079261009ac775bf3e1336ad44fd",
1197-
"portfolio": "ccc282fc1caec348531363f6acf0edd53afd6ff3a65f32c3b79a3d5e2e273a85",
1198-
"taxonomy": "d8f3a3e9728cd04929750e8858e100fd60a4a0afc095b0217ac0877b02f0a7f2"
1197+
"portfolio": "3d0fd37ac5a964a077c160983998a54ce9c23a929e3783db65023f999e112cfc",
1198+
"taxonomy": "7f6ee8c30f5484b8881e27099af72c69625b73a7cf3b53fe51c8c0c147d4f124"
11991199
}
12001200
}

‎scripts/lib/validate-optimization-controls.js‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -247,16 +247,16 @@ function buildOptimizationControlChecks(ctx) {
247247

248248
const portfolio = buildPortfolio(ROOT)
249249
const portfolioErrors = validatePortfolio(portfolio)
250-
if (portfolio.summary.skillCount !== 86) err(`[V92] expected 86 skills, got ${portfolio.summary.skillCount}`)
250+
if (portfolio.summary.skillCount !== 87) err(`[V92] expected 87 skills, got ${portfolio.summary.skillCount}`)
251251
if (portfolio.summary.graySkillCount !== 3) err(`[V92] expected three gray skills, got ${portfolio.summary.graySkillCount}`)
252252
if (JSON.stringify(portfolio.summary.publicCategoryCounts) !== JSON.stringify({
253253
'workflow-routing': 20,
254-
'domain-architecture': 21,
254+
'domain-architecture': 22,
255255
'quality-delivery': 28,
256256
'runtime-governance': 17
257257
})) err('[V92] public Skill category counts drifted')
258258
if (portfolio.summary.dependencyEdgeCount < 1) err('[V92] explicit Skill dependency graph has no edges')
259-
if (portfolio.summary.operationalEvidenceCompleteCount !== 86) err('[V92] operational lifecycle evidence is incomplete')
259+
if (portfolio.summary.operationalEvidenceCompleteCount !== 87) err('[V92] operational lifecycle evidence is incomplete')
260260
if (portfolio.summary.triggerQuality !== 'mixed') err('[V92] trigger precision must reflect measured kernel samples')
261261
if (!Number.isInteger(portfolio.generatedFrom.consumerInventoryFileCount) || portfolio.generatedFrom.consumerInventoryFileCount < 50) {
262262
err('[V92] portfolio consumer inventory is missing or unexpectedly small')
@@ -275,7 +275,7 @@ function buildOptimizationControlChecks(ctx) {
275275
if (committedText !== serializePortfolio(portfolio)) err('[V92] committed Skill portfolio is stale')
276276

277277
if (process.env.DEVCODEX_VALIDATION_SCOPE === 'source') {
278-
console.log('[V92] optimization source controls checked: skills=86 gray=3; deployed runtime index deferred to profile-deploy')
278+
console.log('[V92] optimization source controls checked: skills=87 gray=3; deployed runtime index deferred to profile-deploy')
279279
return
280280
}
281281
const runtimeState = buildRuntimeStateIndex(ACTIVE_DEVCODEX_ROOT)
@@ -286,7 +286,7 @@ function buildOptimizationControlChecks(ctx) {
286286
err('[V92] runtime-state index must expose transition and consumer-drift diagnostics')
287287
}
288288

289-
console.log(`[V92] optimization controls checked: skills=86 gray=3 runtimeAlerts=${runtimeState.summary.alertCount}`)
289+
console.log(`[V92] optimization controls checked: skills=87 gray=3 runtimeAlerts=${runtimeState.summary.alertCount}`)
290290
}
291291

292292
return { checkV92 }

‎scripts/test-mcp-hot-reload-supervisor.js‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ readline.createInterface({ input: process.stdin }).on('line', line => {
3131
`
3232
}
3333

34-
function callClient (child, onMessage = null) {
34+
function callClient (child, onMessage = null, diagnosticContext = () => ({})) {
3535
const pending = new Map()
3636
readline.createInterface({ input: child.stdout }).on('line', line => {
3737
const message = JSON.parse(line)
@@ -46,7 +46,7 @@ function callClient (child, onMessage = null) {
4646
const requestId = ++id
4747
const timer = setTimeout(() => {
4848
pending.delete(requestId)
49-
reject(new Error(`timeout: ${method}`))
49+
reject(new Error(`timeout: ${method}; requestId=${requestId}; childPid=${child.pid}; childExitCode=${child.exitCode}; context=${JSON.stringify(diagnosticContext())}`))
5050
}, 5000)
5151
pending.set(requestId, message => {
5252
clearTimeout(timer)
@@ -126,7 +126,12 @@ async function main () {
126126
})
127127
child.stderr.on('data', chunk => diagnostics.push(String(chunk)))
128128
const observedMessages = []
129-
const call = callClient(child, message => observedMessages.push(message))
129+
let phase = 'initialize'
130+
const call = callClient(child, message => observedMessages.push(message), () => ({
131+
phase,
132+
diagnostics: diagnostics.join('').slice(-4000),
133+
lastResponseIds: observedMessages.slice(-5).map(message => message.id)
134+
}))
130135
child.stdin.write('42\n')
131136
const invalidDeadline = Date.now() + 5000
132137
while (
@@ -142,10 +147,12 @@ async function main () {
142147
const initialized = await call('initialize', { protocolVersion: '2024-11-05', capabilities: {} })
143148
assert(initialized.result)
144149
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} })}\n`)
150+
phase = 'initial-worker-call'
145151
const first = toolValue(await call('tools/call', { name: 'fixture_version', arguments: {} }))
146152
assert.strictEqual(first.version, 'v1')
147153

148154
fs.writeFileSync(worker, workerSource('v2'))
155+
phase = 'implementation-hot-reload'
149156
const second = await waitForVersion(call, 'v2')
150157
assert.notStrictEqual(second.pid, first.pid, 'worker PID must change while supervisor connection stays open')
151158

@@ -163,14 +170,17 @@ async function main () {
163170
await new Promise(resolve => setTimeout(resolve, 10))
164171
}
165172
assert.strictEqual(observedMessages.find(message => message.id === 1000)?.error?.data?.errorCode, 'MCP_STDIO_FRAME_TOO_LARGE', 'internal id expansion must not produce a worker timeout')
173+
phase = 'after-frame-rejection'
166174
assert.strictEqual(toolValue(await call('tools/call')).version, 'v2')
167175
process.kill(second.pid)
168176
await new Promise(resolve => setTimeout(resolve, 100))
177+
phase = 'same-contract-crash-recovery'
169178
const restarted = toolValue(await call('tools/call'))
170179
assert.strictEqual(restarted.version, 'v2', 'same contract crash recovery must work')
171180

172181
fs.writeFileSync(worker, workerSource('v3', 'changed_schema'))
173182
await new Promise(resolve => setTimeout(resolve, 150))
183+
phase = 'changed-contract-hot-reload'
174184
const afterSchemaChange = toolValue(await call('tools/call', { name: 'fixture_version', arguments: {} }))
175185
assert.strictEqual(afterSchemaChange.version, 'v2', 'schema change must keep the compatible worker serving')
176186
await new Promise(resolve => setTimeout(resolve, 50))
@@ -180,6 +190,7 @@ async function main () {
180190

181191
process.kill(restarted.pid)
182192
await new Promise(resolve => setTimeout(resolve, 100))
193+
phase = 'changed-contract-crash-recovery'
183194
const incompatibleRestart = await call('tools/call')
184195
assert.strictEqual(incompatibleRestart.error?.data?.errorCode, 'MCP_SCHEMA_RESTART_REQUIRED')
185196
assert.strictEqual((await call('tools/list')).error?.data?.errorCode, 'MCP_SCHEMA_RESTART_REQUIRED', 'rejected crash worker cannot serve the changed contract')

‎scripts/test-validate-profile.js‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,11 @@ function runValidateAll(workspaceRoot) {
105105
})
106106
}
107107

108-
function runValidateCurrent(workspaceRoot) {
108+
function runValidateCurrent(workspaceRoot, env = process.env) {
109109
return spawnSync(process.execPath, [ALL_SCRIPT, '--workspace', workspaceRoot, '--current-only'], {
110110
cwd: workspaceRoot,
111-
encoding: 'utf8'
111+
encoding: 'utf8',
112+
env
112113
})
113114
}
114115

@@ -1272,6 +1273,23 @@ function main() {
12721273
assert.match(workspaceDebtOutput, /\[profile-all\] current-project=devcodex checked=1 errors=0 warnings=0/)
12731274
assert.match(workspaceDebtOutput, /\[profile-all\] workspace-debt checked=2 errors=1 warnings=0/)
12741275
assert.match(workspaceDebtOutput, /profile-standard requires 04-测试规范\.md/)
1276+
const bareSourceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'devcodex-profile-source-'))
1277+
TEMP_ROOTS.push(bareSourceRoot)
1278+
const sourceEnv = { ...process.env, DEVCODEX_VALIDATION_SCOPE: 'source' }
1279+
const sourceWithoutWorkspace = runValidateCurrent(bareSourceRoot, sourceEnv)
1280+
assert.strictEqual(sourceWithoutWorkspace.status, 0, sourceWithoutWorkspace.stderr)
1281+
assert.match(sourceWithoutWorkspace.stdout, /\[profile-current\] N\/A: source checkout has no deployed workspace Profile/)
1282+
const localWithoutWorkspace = runValidateCurrent(bareSourceRoot, { ...process.env, DEVCODEX_VALIDATION_SCOPE: 'workspace' })
1283+
assert.strictEqual(localWithoutWorkspace.status, 1)
1284+
assert.match(localWithoutWorkspace.stderr, /missing \.devcodex directory/)
1285+
const deployedWithoutCurrent = runValidateCurrent(allProfilesRoot, sourceEnv)
1286+
assert.strictEqual(deployedWithoutCurrent.status, 1)
1287+
assert.doesNotMatch(deployedWithoutCurrent.stdout, /N\/A/)
1288+
writeFile(currentOnlyRoot, '.devcodex/devcodex/profile/01-项目信息.md', [currentProjectInfo(), '', '- Profile 档位:profile-standard。'].join('\n'))
1289+
const invalidDeployedCurrent = runValidateCurrent(currentOnlyRoot, sourceEnv)
1290+
assert.strictEqual(invalidDeployedCurrent.status, 1)
1291+
assert.match(invalidDeployedCurrent.stdout, /profile-standard requires 04-测试规范\.md/)
1292+
assert.doesNotMatch(invalidDeployedCurrent.stdout, /N\/A/)
12751293
const batchEResult = spawnSync(process.execPath, [path.join(ROOT, 'scripts', 'test-v1178-batch-e.js')], {
12761294
cwd: ROOT,
12771295
encoding: 'utf8'

‎scripts/test-validation-dag.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2245,6 +2245,7 @@ function run() {
22452245
const profileCurrentNode = manifest.nodes.find(node => node.id === 'profile-current')
22462246
assert(profileCurrentNode, 'manifest must expose lightweight profile-current node')
22472247
assert.deepStrictEqual(profileCurrentNode.args, ['scripts/validate-all-profiles.js', '--current-only'])
2248+
assert.deepStrictEqual(profileCurrentNode.environment, { DEVCODEX_VALIDATION_SCOPE: 'source' })
22482249
assert.strictEqual(profileCurrentNode.riskClass, 'normal')
22492250
assert.deepStrictEqual(profileCurrentNode.writeScopes, [])
22502251
assert(manifest.iterativeInvariantNodes.includes('profile-current'),

‎scripts/validate-all-profiles.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ const sourceProjectRoot = path.join(workspaceRoot, path.basename(ROOT))
3535

3636
function collectProfileTargets() {
3737
if (!fs.existsSync(devcodexRoot)) {
38+
if (currentOnly && process.env.DEVCODEX_VALIDATION_SCOPE === 'source') {
39+
console.log(`[profile-current] N/A: source checkout has no deployed workspace Profile: ${devcodexRoot}`)
40+
process.exit(0)
41+
}
3842
console.error(`[profile-all] missing .devcodex directory: ${devcodexRoot}`)
3943
process.exit(1)
4044
}

0 commit comments

Comments
 (0)