Skip to content

Commit a8b6dec

Browse files
committed
fix: harden profile tier detection
1 parent a453c2a commit a8b6dec

3 files changed

Lines changed: 72 additions & 6 deletions

File tree

‎changelogs/unreleased.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
## 当前未发布变更
1010

11-
暂无。
11+
- 2026-07-09:修复 `validate-profile` 档位检测在 workspace fallback 与项目本地 Profile 混合时可能被 fallback `profile-lite` 误导的问题;项目本地显式 `Profile 档位` 现在优先,新增回归测试覆盖 standard 项缺少 04/05 时必须失败,支撑 `ProfileTierStandardGate` / `AllDevCodexProfileValidationGate` 的真实退出码验证。
1212

1313
## 记录提示
1414

‎scripts/test-validate-profile.js‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,36 @@ function main() {
477477
assert.strictEqual(explicitFallbackResult.status, 0, explicitFallbackOutput)
478478
assert.doesNotMatch(explicitFallbackOutput, /missing required/)
479479

480+
const localTierOverridesFallbackRoot = createWorkspaceNamespaceWorkspace(currentProjectInfo())
481+
const localStandardProfileDir = path.join(localTierOverridesFallbackRoot, '.devcodex', 'sample', 'profile')
482+
writeFile(localTierOverridesFallbackRoot, '.devcodex/sample/profile/01-项目信息.md', [
483+
currentProjectInfo(),
484+
'',
485+
'- Profile 档位:profile-standard。'
486+
].join('\n'))
487+
fs.mkdirSync(path.join(localTierOverridesFallbackRoot, 'sample'), { recursive: true })
488+
writeFile(localTierOverridesFallbackRoot, 'sample/package.json', JSON.stringify({
489+
name: 'sample',
490+
version: '0.0.1',
491+
scripts: { test: 'node -e "1"' }
492+
}, null, 2))
493+
const localTierOverridesFallbackResult = runValidateWithArgs(
494+
path.join(localTierOverridesFallbackRoot, 'sample'),
495+
[
496+
'--profile-dir',
497+
localStandardProfileDir,
498+
'--workspace-profile',
499+
path.join(localTierOverridesFallbackRoot, '.devcodex', 'workspace', 'profile'),
500+
'--project-root',
501+
path.join(localTierOverridesFallbackRoot, 'sample')
502+
]
503+
)
504+
const localTierOverridesFallbackOutput = `${localTierOverridesFallbackResult.stdout}\n${localTierOverridesFallbackResult.stderr}`
505+
506+
assert.strictEqual(localTierOverridesFallbackResult.status, 1, localTierOverridesFallbackOutput)
507+
assert.match(localTierOverridesFallbackOutput, /profile-standard requires 04-测试规范\.md/)
508+
assert.match(localTierOverridesFallbackOutput, /profile-standard requires 05-交付发布规范\.md or 05-发布规范\.md/)
509+
480510
const allProfilesRoot = createWorkspaceNamespaceWorkspace(currentProjectInfo())
481511
writeFile(allProfilesRoot, '.devcodex/chat/profile/01-项目信息.md', currentProjectInfo())
482512
const allProfilesResult = runValidateAll(allProfilesRoot)

‎scripts/validate-profile.js‎

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,19 @@ function profileCorpus(fileNames) {
142142
.join('\n')
143143
}
144144

145+
function profileCorpusEntries(fileNames) {
146+
return fileNames.map(fileName => {
147+
const info = profileFileInfo(fileName)
148+
const text = info.source === 'missing' ? '' : fs.readFileSync(info.path, 'utf8')
149+
return { fileName, info, text }
150+
})
151+
}
152+
153+
function extractExplicitProfileTiers(text) {
154+
return [...String(text || '').matchAll(/Profile\s*档位[::]\s*`?(profile-(?:lite|standard|closed-loop))`?/gi)]
155+
.map(match => match[1])
156+
}
157+
145158
function hasCurrentAgentsDistribution(text) {
146159
return /devcodex\/agents\/\s*→\s*\.github\/agents\//.test(text) &&
147160
/(Copilot[^\n。]*默认分发|默认分发[^\n。]*Copilot)/.test(text) &&
@@ -594,7 +607,7 @@ const PROFILE_TIER_GATES = [
594607
]
595608

596609
function detectProfileTier() {
597-
const combined = profileCorpus([
610+
const tierFiles = [
598611
'README.md',
599612
'01-项目信息.md',
600613
'02-架构约束.md',
@@ -604,11 +617,34 @@ function detectProfileTier() {
604617
'05-发布规范.md',
605618
'06-功能清单.md',
606619
'07-用户文档与契约规范.md'
607-
])
608-
const explicitTier = combined.match(/Profile\s*档位[::]\s*`?(profile-(?:lite|standard|closed-loop))`?/i)
609-
if (explicitTier) {
610-
return { tier: explicitTier[1], combined }
620+
]
621+
const entries = profileCorpusEntries(tierFiles)
622+
const combined = entries
623+
.map(entry => `\n--- ${entry.fileName} (${entry.info.source}) ---\n${entry.text}`)
624+
.join('\n')
625+
626+
const projectTiers = entries
627+
.filter(entry => entry.info.source === 'project')
628+
.flatMap(entry => extractExplicitProfileTiers(entry.text))
629+
const uniqueProjectTiers = [...new Set(projectTiers)]
630+
if (uniqueProjectTiers.length > 1) {
631+
warn(`[profile] multiple project-local profile tiers declared: ${uniqueProjectTiers.join(', ')}`)
632+
}
633+
if (uniqueProjectTiers.length >= 1) {
634+
return { tier: uniqueProjectTiers[0], combined }
635+
}
636+
637+
const fallbackTiers = entries
638+
.filter(entry => entry.info.source !== 'missing')
639+
.flatMap(entry => extractExplicitProfileTiers(entry.text))
640+
const uniqueFallbackTiers = [...new Set(fallbackTiers)]
641+
if (uniqueFallbackTiers.length > 1) {
642+
warn(`[profile] multiple fallback profile tiers declared: ${uniqueFallbackTiers.join(', ')}`)
643+
}
644+
if (uniqueFallbackTiers.length >= 1) {
645+
return { tier: uniqueFallbackTiers[0], combined }
611646
}
647+
612648
const matches = [...combined.matchAll(/\bprofile-(?:lite|standard|closed-loop)\b/g)].map(match => match[0])
613649
const unique = [...new Set(matches)]
614650
if (unique.length > 1) {

0 commit comments

Comments
 (0)