Skip to content

Commit 31b70d4

Browse files
committed
fix: make release validation portable in clean checkouts
1 parent 44870da commit 31b70d4

11 files changed

Lines changed: 120 additions & 69 deletions

‎changelogs/releases/v1.13.0.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
- 新增 `AgentCapabilityDomainCompletenessGate`、`DocsAudienceRoleAndRenderedSequenceProbe`、`ModulePerformanceCoverageAndMaintenanceGate` 及 V94/V95 正负向探针。
2121
- 新增 `ChecklistStateMaterializationGate` 与 V94 stale snapshot 负向探针,clean/closed 声明前原子核对复审清单六区块,阻断局部回写造成的假收敛。
2222
- 新增 gray `CandidateDiffCompletenessGate`:commit/tag/publish 前物化 staged candidate snapshot,覆盖 tracked/untracked,并用 cached diff、name-status、secret-shape 与 intended scope 对账阻断候选差异假绿;本版本复证只关闭当前事件,长期有效性仍等待后续发布 WorkUnit 前瞻证据。
23+
- 修复 V80~V82 在 CI 干净 checkout 缺少本地 Profile 时无条件读取导致的 `ENOENT`:Profile 存在则继续校验,缺失时明确降级到仓库内消费者证据,并以缺失/存在正反单测守门。
24+
- 修复 V24/V83~V86 的同类 Profile 环境依赖,并将 Skill portfolio 的 Skill hash、sourceDigest、pluginDigest 与字节预算统一按 LF 规范化计算,避免 Windows CRLF 与 Linux LF 让同一提交产生不同 portfolio 结果。
25+
- V2/V19/V28 进一步区分历史运行态证据、缺失的可选 active Profile 与仓库必需证据,避免合法干净检出因 warning-only 退出码阻断 Remote CI,同时继续报告真实断链和不完整 Profile。
2326
- 治理记录改为意图驱动生命周期:中性候选、复合 RecordRouter、成功落盘证据、`RecordNoneChallengeGate` 与跨轮迁移共同防止关键词误触发和漏记。
2427

2528
## 资产与验证面

‎scripts/lib/skill-portfolio-utils.js‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ function sha256(value) {
1111
return crypto.createHash('sha256').update(value).digest('hex')
1212
}
1313

14+
function canonicalizeTextForDigest(value) {
15+
return String(value).replace(/\r\n?/g, '\n')
16+
}
17+
1418
function walk(root) {
1519
if (!fs.existsSync(root)) return []
1620
const out = []
@@ -129,6 +133,7 @@ function buildPortfolio(root) {
129133
const id = path.basename(path.dirname(file))
130134
const source = normalizePath(root, file)
131135
const content = fs.readFileSync(file, 'utf8')
136+
const canonicalContent = canonicalizeTextForDigest(content)
132137
const frontmatter = parseFrontmatter(content, id)
133138
const consumerRows = consumers
134139
.filter(item => item.content.includes(id))
@@ -144,7 +149,7 @@ function buildPortfolio(root) {
144149
const validationProfile = consumerRows
145150
.filter(item => item.role === 'current' && /^scripts\/(?:test-|validate)/.test(item.path))
146151
.map(item => item.path)
147-
const hash = sha256(content)
152+
const hash = sha256(canonicalContent)
148153
sourceRows.push(`${source}:${hash}`)
149154
return {
150155
id,
@@ -186,7 +191,7 @@ function buildPortfolio(root) {
186191
skillsPattern: 'skills/*/SKILL.md',
187192
registry: 'plugin.json',
188193
sourceDigest: sha256(sourceRows.sort().join('\n')),
189-
pluginDigest: sha256(fs.readFileSync(pluginPath))
194+
pluginDigest: sha256(canonicalizeTextForDigest(fs.readFileSync(pluginPath, 'utf8')))
190195
},
191196
ordering: 'skills.id asc; graph edges from/to asc',
192197
summary: {
@@ -196,7 +201,7 @@ function buildPortfolio(root) {
196201
graySkillCount: skills.filter(skill => skill.lifecycleState === 'gray').length,
197202
orphanActiveCount: orphanActive.length,
198203
dependencyCycleCount: cycles.length,
199-
instructionBudgetP95Bytes: percentile(skillFiles.map(file => fs.statSync(file).size), 0.95),
204+
instructionBudgetP95Bytes: percentile(skillFiles.map(file => Buffer.byteLength(canonicalizeTextForDigest(fs.readFileSync(file, 'utf8')), 'utf8')), 0.95),
200205
triggerQuality: 'insufficient-evidence'
201206
},
202207
dependencyGraph: { nodes, edges, cycles },
@@ -235,6 +240,7 @@ function serializePortfolio(portfolio) {
235240

236241
module.exports = {
237242
buildPortfolio,
243+
canonicalizeTextForDigest,
238244
detectCycles,
239245
parseFrontmatter,
240246
serializePortfolio,

‎scripts/lib/test-spec-governance-expert.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ function runSpecGovernanceExpertSuite(ctx) {
99
const checkV83 = 'ProfileTierStandardGate'
1010
for (const [file, needle] of [
1111
['scripts/lib/validate-governance-expert.js', 'checkV83'],
12+
['scripts/lib/validate-governance-expert.js', 'collectActiveProfileCorpus'],
13+
['scripts/lib/validate-governance-expert.js', 'appendActiveProfileProbe'],
14+
['scripts/lib/validate-governance-expert.js', 'active Profile corpus unavailable'],
1215
['scripts/lib/validate-governance-expert.js', checkV83],
1316
['scripts/lib/validate-governance-expert.js', 'ProfileLifecycleClassificationGate'],
1417
['scripts/lib/validate-governance-expert.js', 'AllDevCodexProfileValidationGate'],

‎scripts/lib/test-spec-governance-review.js‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict'
22

3+
const { collectActiveProfileCorpusIfAvailable } = require('./validate-governance-review')
4+
35
function runSpecGovernanceReviewSuite(ctx) {
46
const {
57
ROOT, fs, path, failures, SOURCE_PROJECT_NAME, read, mustInclude,
@@ -178,11 +180,24 @@ function runSpecGovernanceReviewSuite(ctx) {
178180
}
179181

180182
const checkV80 = 'UserManualReviewScope'
183+
const missingProfileCorpus = collectActiveProfileCorpusIfAvailable(
184+
{ existsSync: () => false }, path, '/clean-checkout/.devcodex', () => { throw new Error('missing Profile must not be read') }
185+
)
186+
if (missingProfileCorpus !== null) failures.push('checkV80 missing Profile corpus must degrade to null')
187+
const presentProfileCorpus = collectActiveProfileCorpusIfAvailable(
188+
{ existsSync: () => true }, path, '/workspace/.devcodex', file => path.basename(file)
189+
)
190+
if (!presentProfileCorpus.includes('01-项目信息.md') || !presentProfileCorpus.includes('02-架构约束.md')) {
191+
failures.push('checkV80 present Profile corpus must include both required sources')
192+
}
181193
for (const [file, needle] of [
182194
['scripts/lib/validate-governance-review.js', 'checkV80'],
183195
['scripts/lib/validate-governance-review.js', checkV80],
184196
['scripts/lib/validate-governance-review.js', 'DocsNavigationReviewMatrix'],
185197
['scripts/lib/validate-governance-review.js', 'audit-user-manual'],
198+
['scripts/lib/validate-governance-review.js', 'collectActiveProfileCorpusIfAvailable'],
199+
['scripts/lib/validate-governance-control.js', 'activeProfileAvailable'],
200+
['scripts/lib/validate-governance-control.js', 'active Profile unavailable'],
186201
['scripts/validate.js', 'runProbeRegistry'],
187202
['plugin.json', 'skills/audit-user-manual/SKILL.md'],
188203
['skills/audit-user-manual/SKILL.md', 'UserManualReviewScope'],

‎scripts/lib/validate-core-checks.js‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ function buildValidateCoreChecks(ctx) {
7474
const files = roots.flatMap(r => walk(path.join(ROOT, r))).filter(f => f.endsWith('.md'))
7575
.concat(topFiles.map(f => path.join(ROOT, f)).filter(f => fs.existsSync(f)))
7676
let checked = 0
77+
let historicalRuntimeEvidenceSkipped = 0
7778
for (const f of files) {
7879
let content = read(f)
7980
// strip fenced code blocks and inline code to avoid matching links in examples/templates
@@ -86,13 +87,19 @@ function buildValidateCoreChecks(ctx) {
8687
if (!target || /^https?:|^mailto:|^file:/.test(target)) continue
8788
const base = path.dirname(f)
8889
const abs = path.resolve(base, target)
89-
if (target.endsWith('.md') && !fs.existsSync(abs)) {
90+
const normalizedTarget = target.replace(/\\/g, '/')
91+
const relativeSource = path.relative(ROOT, f).replace(/\\/g, '/')
92+
const isHistoricalRuntimeEvidence = relativeSource.startsWith('changelogs/releases/') &&
93+
normalizedTarget.split('/').includes('.devcodex')
94+
if (target.endsWith('.md') && !fs.existsSync(abs) && isHistoricalRuntimeEvidence) {
95+
historicalRuntimeEvidenceSkipped++
96+
} else if (target.endsWith('.md') && !fs.existsSync(abs)) {
9097
warn(`[V2] Broken link in ${path.relative(ROOT, f)}: ${target}`)
9198
}
9299
checked++
93100
}
94101
}
95-
console.log(`[V2] links scanned: ${checked}`)
102+
console.log(`[V2] links scanned: ${checked}; historical runtime evidence unavailable: ${historicalRuntimeEvidenceSkipped}`)
96103
}
97104

98105
function checkV3() {
@@ -488,9 +495,15 @@ function buildValidateCoreChecks(ctx) {
488495
{ file: 'website/docs/intro/index.md', needle: `${skillCount} 个按需触发的工作流技能` },
489496
{ file: 'website/docs/specs/directory-structure.md', needle: `扁平一级 Skill(${skillCount} 个)` }
490497
]
498+
const activeProfileDirAvailable = fs.existsSync(activePath('profile'))
499+
let optionalActiveProfileChecksSkipped = 0
491500
for (const check of checks) {
492501
const filePath = check.rawPath === false ? check.file : path.join(ROOT, check.file)
493502
if (!fs.existsSync(filePath)) {
503+
if (check.rawPath === false && !activeProfileDirAvailable) {
504+
optionalActiveProfileChecksSkipped++
505+
continue
506+
}
494507
warn(`[V19] asset count source missing, skip: ${path.relative(ROOT, filePath)}`)
495508
continue
496509
}
@@ -514,7 +527,7 @@ function buildValidateCoreChecks(ctx) {
514527
err('[V19] active version CHANGELOG must record the existing template-flow-alignment requirement detail')
515528
}
516529

517-
console.log(`[V19] asset counts checked: instructions=${instructionCount}, skills=${skillCount}, prompts=${promptCount}, hook-runtime=${hookRuntimeCount}, data-templates=${dataTemplateCount}, scripts=${scriptCount}`)
530+
console.log(`[V19] asset counts checked: instructions=${instructionCount}, skills=${skillCount}, prompts=${promptCount}, hook-runtime=${hookRuntimeCount}, data-templates=${dataTemplateCount}, scripts=${scriptCount}; optional active-profile checks skipped=${optionalActiveProfileChecksSkipped}`)
518531
}
519532

520533
function extractSubtypes(content, workflow) {

‎scripts/lib/validate-governance-control.js‎

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -303,13 +303,16 @@ function buildGovernanceControlChecks(ctx) {
303303
}
304304

305305
function checkV24() {
306+
const activeProfileFiles = [
307+
activePath('profile', '01-项目信息.md'),
308+
activePath('profile', '02-架构约束.md')
309+
]
310+
const activeProfileAvailable = activeProfileFiles.every(file => fs.existsSync(file))
306311
const requiredFiles = [
307312
'data/templates/pending-issues.md',
308313
'data/README.md',
309314
'README.md',
310315
'RULES.md',
311-
activePath('profile', '01-项目信息.md'),
312-
activePath('profile', '02-架构约束.md'),
313316
'website/rspress.config.ts',
314317
'website/docs/index.md',
315318
'website/docs/intro/index.md',
@@ -349,13 +352,6 @@ function buildGovernanceControlChecks(ctx) {
349352
{ file: 'RULES.md', needle: 'init --claude' },
350353
{ file: 'RULES.md', needle: 'init --codex' },
351354
{ file: 'RULES.md', needle: 'AGENTS.md' },
352-
{ file: activePath('profile', '01-项目信息.md'), needle: 'CLAUDE.md', rawPath: false },
353-
{ file: activePath('profile', '01-项目信息.md'), needle: 'AGENTS.md', rawPath: false },
354-
{ file: activePath('profile', '01-项目信息.md'), needle: 'pending-issues.md', rawPath: false },
355-
{ file: activePath('profile', '01-项目信息.md'), needle: '当前阶段', rawPath: false },
356-
{ file: activePath('profile', '02-架构约束.md'), needle: '.codex/hooks.json', rawPath: false },
357-
{ file: activePath('profile', '02-架构约束.md'), needle: 'pending-issues', rawPath: false },
358-
{ file: activePath('profile', '02-架构约束.md'), needle: 'process-improvements', rawPath: false },
359355
{ file: 'website/rspress.config.ts', needle: 'Copilot / Claude Code' },
360356
{ file: 'website/docs/index.md', needle: 'Copilot / Claude Code' },
361357
{ file: 'website/docs/index.md', needle: 'Codex' },
@@ -374,6 +370,19 @@ function buildGovernanceControlChecks(ctx) {
374370
{ file: 'scripts/validate-profile.js', needle: 'jetbrains-copilot' },
375371
{ file: 'scripts/lib/cli-maintenance-commands.js', needle: 'jetbrains-copilot' }
376372
]
373+
if (activeProfileAvailable) {
374+
musts.push(
375+
{ file: activeProfileFiles[0], needle: 'CLAUDE.md', rawPath: false },
376+
{ file: activeProfileFiles[0], needle: 'AGENTS.md', rawPath: false },
377+
{ file: activeProfileFiles[0], needle: 'pending-issues.md', rawPath: false },
378+
{ file: activeProfileFiles[0], needle: '当前阶段', rawPath: false },
379+
{ file: activeProfileFiles[1], needle: '.codex/hooks.json', rawPath: false },
380+
{ file: activeProfileFiles[1], needle: 'pending-issues', rawPath: false },
381+
{ file: activeProfileFiles[1], needle: 'process-improvements', rawPath: false }
382+
)
383+
} else {
384+
console.log('[V24] active Profile unavailable — repository governance/client/template files remain authoritative')
385+
}
377386

378387
for (const probe of musts) {
379388
const content = probe.rawPath === false ? read(probe.file) : read(path.join(ROOT, probe.file))

‎scripts/lib/validate-governance-expert.js‎

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,30 @@ function buildGovernanceExpertChecks(ctx) {
1010
} = ctx
1111
const { collectChangelogSources, hasChangelogEvidence } = buildGovernanceHelpers(ctx)
1212

13+
function collectActiveProfileCorpus(names) {
14+
const files = names.map(name => path.join(ACTIVE_DEVCODEX_ROOT, 'profile', name))
15+
if (!files.every(file => fs.existsSync(file))) return null
16+
return files.map(file => read(file)).join('\n')
17+
}
18+
19+
function appendActiveProfileProbe(probes, profileCorpus, needles, probeId) {
20+
if (profileCorpus !== null) probes.push({ file: 'active profile corpus', content: profileCorpus, needles })
21+
else console.log(`[${probeId}] active Profile corpus unavailable — repository consumers remain authoritative`)
22+
}
23+
1324
function checkV83() {
1425
const gates = [
1526
'ProfileTierStandardGate',
1627
'ProfileLifecycleClassificationGate',
1728
'AllDevCodexProfileValidationGate'
1829
]
1930

20-
const profileCorpus = [
31+
const profileCorpus = collectActiveProfileCorpus([
2132
'README.md',
2233
'01-项目信息.md',
2334
'06-功能清单.md',
2435
'07-用户文档与契约规范.md'
25-
]
26-
.map(name => {
27-
const file = path.join(ACTIVE_DEVCODEX_ROOT, 'profile', name)
28-
return fs.existsSync(file) ? read(file) : ''
29-
})
30-
.join('\n')
36+
])
3137
const changelogCorpus = collectChangelogSources()
3238
.map(source => source.content)
3339
.join('\n')
@@ -45,9 +51,9 @@ function buildGovernanceExpertChecks(ctx) {
4551
{ file: 'skills/report/SKILL.md', needles: ['ProfileTierValidation', 'AllDevCodexProfileValidation'].concat(gates) },
4652
{ file: 'README.md', needles: ['profile-lite', 'profile-standard', 'profile-closed-loop', 'AllDevCodexProfileValidationGate'].concat(gates) },
4753
{ file: 'website/docs/guide/development.md', needles: ['profile-lite', 'profile-standard', 'profile-closed-loop', 'AllDevCodexProfileValidationGate'].concat(gates) },
48-
{ file: 'active profile corpus', content: profileCorpus, needles: ['profile-closed-loop', '06-功能清单', '07-用户文档与契约规范', '稳定基线', '活文档'].concat(gates) },
4954
{ file: 'changelog corpus', content: changelogCorpus, needles: ['V83'].concat(gates) }
5055
]
56+
appendActiveProfileProbe(probes, profileCorpus, ['profile-closed-loop', '06-功能清单', '07-用户文档与契约规范', '稳定基线', '活文档'].concat(gates), 'V83')
5157

5258
for (const probe of probes) {
5359
const content = probe.content || read(path.join(ROOT, probe.file))
@@ -78,16 +84,11 @@ function buildGovernanceExpertChecks(ctx) {
7884
'ExpertEvidenceMatrixGate'
7985
]
8086

81-
const profileCorpus = [
87+
const profileCorpus = collectActiveProfileCorpus([
8288
'01-项目信息.md',
8389
'02-架构约束.md',
8490
'06-功能清单.md'
85-
]
86-
.map(name => {
87-
const file = path.join(ACTIVE_DEVCODEX_ROOT, 'profile', name)
88-
return fs.existsSync(file) ? read(file) : ''
89-
})
90-
.join('\n')
91+
])
9192
const changelogCorpus = collectChangelogSources()
9293
.map(source => source.content)
9394
.join('\n')
@@ -131,9 +132,9 @@ function buildGovernanceExpertChecks(ctx) {
131132
{ file: 'website/docs/versions/v1/1.0.1/CHANGELOG.md', needles: ['V84', 'expert-output-quality', 'ExpertOutputQualityGate'] },
132133
{ file: 'scripts/lib/test-spec-governance-expert.js', needles: ['checkV84', 'classifyExpertOutputSample', 'ExpertOutputQualityGate'] },
133134
{ file: 'scripts/validate.js', needles: ['createProbeRegistry', 'expectedProbeIds', 'runProbeRegistry'] },
134-
{ file: 'active profile corpus', content: profileCorpus, needles: ['76', 'expert-output-quality', 'ExpertOutputQualityGate'] },
135135
{ file: 'changelog corpus', content: changelogCorpus, needles: ['V84', 'expert-output-quality', 'ExpertOutputQualityGate'] }
136136
]
137+
appendActiveProfileProbe(probes, profileCorpus, ['76', 'expert-output-quality', 'ExpertOutputQualityGate'], 'V84')
137138

138139
for (const probe of probes) {
139140
const content = probe.content || read(path.join(ROOT, probe.file))
@@ -204,16 +205,11 @@ function buildGovernanceExpertChecks(ctx) {
204205
}
205206
}
206207

207-
const profileCorpus = [
208+
const profileCorpus = collectActiveProfileCorpus([
208209
'01-项目信息.md',
209210
'02-架构约束.md',
210211
'06-功能清单.md'
211-
]
212-
.map(name => {
213-
const file = path.join(ACTIVE_DEVCODEX_ROOT, 'profile', name)
214-
return fs.existsSync(file) ? read(file) : ''
215-
})
216-
.join('\n')
212+
])
217213
const changelogCorpus = collectChangelogSources()
218214
.map(source => source.content)
219215
.join('\n')
@@ -240,9 +236,9 @@ function buildGovernanceExpertChecks(ctx) {
240236
{ file: 'website/docs/versions/v1/1.0.1/CHANGELOG.md', needles: ['V85', 'ExpertOwnerSkillGate'].concat(skillNames) },
241237
{ file: 'scripts/lib/test-spec-governance-expert.js', needles: ['checkV85', 'classifyExpertOwnerSample', 'ExpertOwnerSkillGate'] },
242238
{ file: 'scripts/validate.js', needles: ['createProbeRegistry', 'expectedProbeIds', 'runProbeRegistry'] },
243-
{ file: 'active profile corpus', content: profileCorpus, needles: ['74', '21 个专家 Owner Skill', 'ExpertOwnerSkillGate', 'V85'].concat(skillNames) },
244239
{ file: 'changelog corpus', content: changelogCorpus, needles: ['V85', 'ExpertOwnerSkillGate'].concat(skillNames) }
245240
]
241+
appendActiveProfileProbe(probes, profileCorpus, ['74', '21 个专家 Owner Skill', 'ExpertOwnerSkillGate', 'V85'].concat(skillNames), 'V85')
246242

247243
for (const [name, gate, fields] of skillMap) {
248244
probes.push({
@@ -274,16 +270,11 @@ function buildGovernanceExpertChecks(ctx) {
274270
err('[V86] positive memory bootstrap sample must be file-truth-required')
275271
}
276272

277-
const profileCorpus = [
273+
const profileCorpus = collectActiveProfileCorpus([
278274
'01-项目信息.md',
279275
'06-功能清单.md',
280276
'07-用户文档与契约规范.md'
281-
]
282-
.map(name => {
283-
const file = path.join(ACTIVE_DEVCODEX_ROOT, 'profile', name)
284-
return fs.existsSync(file) ? read(file) : ''
285-
})
286-
.join('\n')
277+
])
287278
const changelogCorpus = collectChangelogSources()
288279
.map(source => source.content)
289280
.join('\n')
@@ -302,9 +293,9 @@ function buildGovernanceExpertChecks(ctx) {
302293
{ file: 'website/docs/versions/v1/1.0.1/CHANGELOG.md', needles: [gate, 'V86'] },
303294
{ file: 'scripts/lib/test-spec-governance-expert.js', needles: ['checkV86', 'classifyMemoryBootstrapSample', gate] },
304295
{ file: 'scripts/validate.js', needles: ['createProbeRegistry', 'expectedProbeIds', 'runProbeRegistry'] },
305-
{ file: 'active profile corpus', content: profileCorpus, needles: [gate, 'V86'] },
306296
{ file: 'changelog corpus', content: changelogCorpus, needles: [gate, 'V86'] }
307297
]
298+
appendActiveProfileProbe(probes, profileCorpus, [gate, 'V86'], 'V86')
308299

309300
for (const probe of probes) {
310301
const content = probe.content || read(path.join(ROOT, probe.file))

0 commit comments

Comments
 (0)