Skip to content

Commit 8b56575

Browse files
committed
fix: harden profile lifecycle validation
1 parent 4da8116 commit 8b56575

4 files changed

Lines changed: 116 additions & 11 deletions

File tree

‎changelogs/unreleased.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
### 2026-07-15
1212

13+
- 修复 `ProfileLifecycleClassificationGate` 的可执行语义:共享 contract 改为稳定基线/活文档/条件或本地文档三类独立 inspection,validator 输出逐类缺项,新增无关“本地/required/conditional”、三类单缺失、中英文完整声明与 workspace fallback/overlay 负向/正向夹具;同步修正 monSQLize closed-loop Profile 和原 F-012 尾项,不改变 Profile 档位或 CLI 契约。
1314
- 实施 ISSUE-043 的 Turn Liveness P0/P1/P2:新增状态机、工具 lease、continuation ACK、checkpoint、`TurnRecoveryCard` 与跨重启重放,接入 `ai-agent-system-architecture` / `host-contract-verification` / `execution-contract` / `rework-prevention-engineering` / TestRoute / report / prompts / Profile,并以 V98、runtime replay、gray sidecar zero-write 与 package fixture 守门;Hook 仅具备事件到达时的历史停滞识别能力,sidecar 保持 one-shot gray,禁止自动唤醒、状态写入、进程控制和未知副作用重放。
1415
- 新增 gray `brand-visual-quality` 与 `BrandVisualQualityGate`,覆盖 MasterLineage、ThemeGeometryParity、MicroOpticalVariant、MonoMaster、VisualEvidencePack、VisualBlockerReset;以 V97 正负 fixture 和独立前向试用守门,未形成真实 WorkUnit 前不晋级 active。
1516
- `RuntimeStateTransitionProjectionGate` 将 append-only 历史迁移、当前权威投影、低权威 consumer drift 和真正 `CONFLICTING_CURRENT_STATE` 分离,保留 schema v1/read-only 兼容并降低 strict conflict 噪声。

‎mcp/profile-contract.js‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,11 +197,26 @@ function inspectFeatureInventoryDocument(markdown, { requireV1 = false } = {}) {
197197
}
198198
}
199199

200-
function hasProfileLifecycle(corpus) {
200+
function inspectProfileLifecycle(corpus) {
201201
const text = String(corpus || '')
202-
return /stable baseline|稳定基线/i.test(text) &&
203-
/living document|活文档/i.test(text) &&
204-
/conditional|required|conditional-required|条件必需|条件\s*[\/]\s*本地|本地/i.test(text)
202+
const result = {
203+
stableBaseline: /\bstable[\s-]+baseline\b|稳定基线/i.test(text),
204+
livingDocument: /\bliving[\s-]+documents?\b|活文档/i.test(text),
205+
conditionalOrLocalDocs: /\bconditional(?:-required)?\s*(?:(?:\/|or|and)\s*)?local\s+docs?\b|条件(?:必需)?\s*(?:\/|或|和)\s*本地文档/i.test(text)
206+
}
207+
const missing = []
208+
if (!result.stableBaseline) missing.push('stable-baseline')
209+
if (!result.livingDocument) missing.push('living-document')
210+
if (!result.conditionalOrLocalDocs) missing.push('conditional-or-local-docs')
211+
return {
212+
...result,
213+
missing,
214+
valid: missing.length === 0
215+
}
216+
}
217+
218+
function hasProfileLifecycle(corpus) {
219+
return inspectProfileLifecycle(corpus).valid
205220
}
206221

207222
function inspectProfileContract(tier, availableFiles, corpus = '', documents = {}) {
@@ -266,6 +281,7 @@ module.exports = {
266281
parseMarkdownTables,
267282
inspectFeatureInventoryDocument,
268283
hasFeatureInventorySource,
284+
inspectProfileLifecycle,
269285
hasProfileLifecycle,
270286
inspectProfileContract
271287
}

‎scripts/test-validate-profile.js‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,21 @@ function createWorkspaceNamespaceWorkspace(projectInfo) {
106106
return root
107107
}
108108

109+
function createClosedLoopWorkspaceNamespace(projectInfo) {
110+
const root = createWorkspaceNamespaceWorkspace(projectInfo)
111+
writeFile(root, '.devcodex/workspace/profile/README.md', [
112+
'# README',
113+
'',
114+
'- Profile 档位:profile-closed-loop。',
115+
'- 生命周期:01~03 为稳定基线;04~07 为活文档;config.local.json 与 08+ 为条件 / 本地文档。'
116+
].join('\n'))
117+
writeFile(root, '.devcodex/workspace/profile/04-测试规范.md', '# 04 — 测试规范\n')
118+
writeFile(root, '.devcodex/workspace/profile/05-发布规范.md', '# 05 — 发布规范\n')
119+
writeFile(root, '.devcodex/workspace/profile/06-功能清单.md', featureInventoryDocument())
120+
writeFile(root, '.devcodex/workspace/profile/07-用户文档与契约规范.md', '# 07 — 用户文档与契约规范\n')
121+
return root
122+
}
123+
109124
function legacyProjectInfo() {
110125
return [
111126
'# 01 — 项目信息',
@@ -174,6 +189,22 @@ function currentProjectInfo() {
174189
].join('\n')
175190
}
176191

192+
function createClosedLoopWorkspace(lifecycle, readmeExtras = []) {
193+
const root = createWorkspace(currentProjectInfo())
194+
writeFile(root, '.devcodex/profile/README.md', [
195+
'# README',
196+
'',
197+
'- Profile 档位:profile-closed-loop。',
198+
`- 生命周期:${lifecycle}。`,
199+
...readmeExtras
200+
].join('\n'))
201+
writeFile(root, '.devcodex/profile/04-测试规范.md', '# 04 — 测试规范\n')
202+
writeFile(root, '.devcodex/profile/05-发布规范.md', '# 05 — 发布规范\n')
203+
writeFile(root, '.devcodex/profile/06-功能清单.md', featureInventoryDocument())
204+
writeFile(root, '.devcodex/profile/07-用户文档与契约规范.md', '# 07 — 用户文档与契约规范\n')
205+
return root
206+
}
207+
177208
function validLocalConfig() {
178209
return JSON.stringify({
179210
connections: {
@@ -424,6 +455,48 @@ function main() {
424455

425456
assert.strictEqual(closedLoopResult.status, 0, closedLoopOutput)
426457

458+
const closedLoopChineseRoot = createClosedLoopWorkspace('01~03 为稳定基线;04~07 为活文档;config.local.json 与 08+ 为条件 / 本地文档')
459+
const closedLoopChineseResult = runValidate(closedLoopChineseRoot)
460+
assert.strictEqual(
461+
closedLoopChineseResult.status,
462+
0,
463+
`${closedLoopChineseResult.stdout}\n${closedLoopChineseResult.stderr}`
464+
)
465+
466+
const lifecycleDiagnosticCases = [
467+
{
468+
name: 'stable-baseline',
469+
lifecycle: 'living document / conditional-required local docs',
470+
expected: 'stable-baseline'
471+
},
472+
{
473+
name: 'living-document',
474+
lifecycle: 'stable baseline / conditional-required local docs',
475+
expected: 'living-document'
476+
},
477+
{
478+
name: 'conditional-or-local-docs',
479+
lifecycle: 'stable baseline / living document',
480+
extras: [
481+
'- 本地命令:npm test。',
482+
'- required checks remain enabled。',
483+
'- conditional workflows are documented elsewhere。'
484+
],
485+
expected: 'conditional-or-local-docs'
486+
}
487+
]
488+
for (const diagnosticCase of lifecycleDiagnosticCases) {
489+
const diagnosticRoot = createClosedLoopWorkspace(diagnosticCase.lifecycle, diagnosticCase.extras)
490+
const diagnosticResult = runValidate(diagnosticRoot)
491+
const diagnosticOutput = `${diagnosticResult.stdout}\n${diagnosticResult.stderr}`
492+
assert.strictEqual(diagnosticResult.status, 1, `${diagnosticCase.name}: ${diagnosticOutput}`)
493+
assert.match(diagnosticOutput, new RegExp(`lifecycle missing ${diagnosticCase.expected}`))
494+
for (const category of ['stable-baseline', 'living-document', 'conditional-or-local-docs']) {
495+
if (category === diagnosticCase.expected) continue
496+
assert.doesNotMatch(diagnosticOutput, new RegExp(`lifecycle missing ${category}`))
497+
}
498+
}
499+
427500
const missingColumnsRoot = createWorkspace(currentProjectInfo())
428501
writeFile(missingColumnsRoot, '.devcodex/profile/README.md', '# README\n\n- Profile 档位:profile-closed-loop。\n- 生命周期:stable baseline / living document / conditional-required local docs。\n')
429502
writeFile(missingColumnsRoot, '.devcodex/profile/04-测试规范.md', '# 04\n')
@@ -677,6 +750,22 @@ function main() {
677750
assert.strictEqual(explicitFallbackResult.status, 0, explicitFallbackOutput)
678751
assert.doesNotMatch(explicitFallbackOutput, /missing required/)
679752

753+
const closedLoopFallbackRoot = createClosedLoopWorkspaceNamespace(currentProjectInfo())
754+
const closedLoopFallbackProfileDir = path.join(closedLoopFallbackRoot, '.devcodex', 'chat', 'profile')
755+
writeFile(closedLoopFallbackRoot, '.devcodex/chat/profile/01-项目信息.md', currentProjectInfo())
756+
const closedLoopFallbackResult = runValidateWithArgs(
757+
path.join(closedLoopFallbackRoot, 'chat'),
758+
[
759+
'--profile-dir',
760+
closedLoopFallbackProfileDir,
761+
'--workspace-profile',
762+
path.join(closedLoopFallbackRoot, '.devcodex', 'workspace', 'profile')
763+
]
764+
)
765+
const closedLoopFallbackOutput = `${closedLoopFallbackResult.stdout}\n${closedLoopFallbackResult.stderr}`
766+
assert.strictEqual(closedLoopFallbackResult.status, 0, closedLoopFallbackOutput)
767+
assert.doesNotMatch(closedLoopFallbackOutput, /lifecycle missing/)
768+
680769
const localTierOverridesFallbackRoot = createWorkspaceNamespaceWorkspace(currentProjectInfo())
681770
const localStandardProfileDir = path.join(localTierOverridesFallbackRoot, '.devcodex', 'sample', 'profile')
682771
writeFile(localTierOverridesFallbackRoot, '.devcodex/sample/profile/01-项目信息.md', [

‎scripts/validate-profile.js‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const {
1818
PROFILE_TIERS,
1919
extractProfileTierDeclarations,
2020
hasFeatureInventorySource: contractHasFeatureInventorySource,
21-
hasProfileLifecycle: contractHasProfileLifecycle,
21+
inspectProfileLifecycle,
2222
inspectFeatureInventoryDocument,
2323
parseMarkdownTables,
2424
FEATURE_INVENTORY_COLUMN_LABELS
@@ -27,6 +27,8 @@ const { resolveProfileDir } = require('../hooks/_runtime/workspace-layout.cjs')
2727

2828
// ProfileGenerationContractGate / FeatureInventorySchemaGate / ProfileTierMigrationSafetyGate
2929
// share the tier vocabulary: profile-lite | profile-standard | profile-closed-loop.
30+
// ProfileLifecycleClassificationGate keeps the historical "conditional-required/local docs" contract anchor;
31+
// user diagnostics expose its precise category key as "conditional-or-local-docs".
3032

3133
const PLUGIN_ROOT = path.resolve(__dirname, '..')
3234
const args = process.argv.slice(2)
@@ -771,10 +773,6 @@ function validateCanonicalFeatureInventory(projectInfo, inventoryResult) {
771773
}
772774
}
773775

774-
function hasProfileLifecycle(combined) {
775-
return contractHasProfileLifecycle(combined)
776-
}
777-
778776
function validateProfileTier(tier, combined) {
779777
if (tier === 'profile-lite') return
780778

@@ -798,8 +796,9 @@ function validateProfileTier(tier, combined) {
798796
if (!hasProfileFile('07-用户文档与契约规范.md')) {
799797
err('[profile] profile-closed-loop requires 07-用户文档与契约规范.md')
800798
}
801-
if (!hasProfileLifecycle(combined)) {
802-
err('[profile] profile-closed-loop requires lifecycle wording for stable baseline, living document and conditional-required/local docs')
799+
const lifecycle = inspectProfileLifecycle(combined)
800+
for (const missing of lifecycle.missing) {
801+
err(`[profile] profile-closed-loop lifecycle missing ${missing}`)
803802
}
804803
}
805804
validateCanonicalFeatureInventory(projectInfoText, inventoryResult)

0 commit comments

Comments
 (0)