release: gate August 1 content candidates
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'node:path'
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const rootDir = path.resolve(scriptDir, '..')
|
||||
const candidatePath = path.join(
|
||||
rootDir,
|
||||
'docs',
|
||||
'content-candidates',
|
||||
'aug1-life-advisor-content-candidates-v0.1.json',
|
||||
)
|
||||
const content = JSON.parse(await readFile(candidatePath, 'utf8'))
|
||||
|
||||
const failures = []
|
||||
const check = (condition, message) => {
|
||||
if (!condition) failures.push(message)
|
||||
}
|
||||
|
||||
const requiredDimensions = new Set([
|
||||
'task_completion',
|
||||
'standardization',
|
||||
'emotion',
|
||||
'response_timeliness',
|
||||
'marketing',
|
||||
])
|
||||
const requiredCategories = new Map([
|
||||
['NORMAL', 6],
|
||||
['SYNONYM', 6],
|
||||
['CONFLICT', 6],
|
||||
['NO_EVIDENCE', 6],
|
||||
['UNAUTHORIZED', 6],
|
||||
])
|
||||
const placeholderPattern = /待补|待提供|待完善|待定|占位内容|占位文本|占位符|placeholder|示例来源|示例内容|待确认|\b(?:todo|tbd)\b/i
|
||||
const phonePattern = /(?<!\d)1[3-9]\d{9}(?!\d)/
|
||||
const preciseRoomPattern = /(?:\d{1,3}\s*(?:栋|幢|单元|室)|\d{1,3}-\d{1,4}\s*室)/
|
||||
const credentialPattern = /(?:门禁|家门|开门|访问)\s*(?:密码|口令|验证码)/
|
||||
|
||||
check(content.candidateStatus === 'PENDING_BUSINESS_REVIEW', 'root status must remain pending review')
|
||||
check(content.publishable === false, 'root publishable must be false')
|
||||
check(Array.isArray(content.scenarios) && content.scenarios.length === 5, 'exactly five scenario candidates are required')
|
||||
check(
|
||||
Array.isArray(content.policyQuestionCandidates) && content.policyQuestionCandidates.length === 30,
|
||||
'exactly thirty policy question candidates are required',
|
||||
)
|
||||
|
||||
for (const scenario of content.scenarios ?? []) {
|
||||
const label = scenario.candidateId ?? 'unknown scenario'
|
||||
const draft = scenario.scenarioDraft ?? {}
|
||||
const rubric = scenario.rubricDraft ?? {}
|
||||
const requiredText = [
|
||||
draft.name,
|
||||
draft.position,
|
||||
draft.scenarioType,
|
||||
draft.projectType,
|
||||
draft.growthLevel,
|
||||
draft.competencyCode,
|
||||
draft.irritants,
|
||||
draft.sopRefs,
|
||||
draft.successCriteria,
|
||||
draft.goal,
|
||||
]
|
||||
|
||||
check(scenario.candidateStatus === 'PENDING_BUSINESS_REVIEW', `${label}: status must remain pending review`)
|
||||
check(scenario.publishable === false, `${label}: publishable must be false`)
|
||||
check(draft.enabled === false, `${label}: scenario must be disabled`)
|
||||
check(draft.riskLevel === '待评估', `${label}: risk must remain pending until business review`)
|
||||
check(rubric.enabled === false, `${label}: rubric must be disabled`)
|
||||
check(placeholderPattern.test(draft.sopRefs ?? ''), `${label}: missing-source marker must remain explicit`)
|
||||
check((scenario.sourceRefs ?? []).length >= 2, `${label}: at least two source references are required`)
|
||||
check((scenario.requiredReviewers ?? []).length >= 2, `${label}: at least two reviewer roles are required`)
|
||||
check((scenario.reviewChecklist ?? []).length >= 4, `${label}: review checklist is incomplete`)
|
||||
check(requiredText.every((value) => typeof value === 'string' && value.trim()), `${label}: required scenario fields are incomplete`)
|
||||
check(Array.isArray(draft.rounds) && draft.rounds.length >= 2, `${label}: at least two rounds are required`)
|
||||
for (const [index, round] of (draft.rounds ?? []).entries()) {
|
||||
check(Boolean(round?.customer?.trim()), `${label}: round ${index + 1} customer text is missing`)
|
||||
check(Boolean(round?.coachHint?.trim()), `${label}: round ${index + 1} coach hint is missing`)
|
||||
}
|
||||
|
||||
const dimensions = rubric.dimensions ?? []
|
||||
check(dimensions.length === 5, `${label}: rubric must contain exactly five dimensions`)
|
||||
check(
|
||||
new Set(dimensions.map((item) => item.code)).size === 5
|
||||
&& dimensions.every((item) => requiredDimensions.has(item.code)),
|
||||
`${label}: rubric dimension codes are incomplete or duplicated`,
|
||||
)
|
||||
const weight = dimensions.reduce((sum, item) => sum + Number(item.weight ?? 0), 0)
|
||||
check(Math.abs(weight - 1) < 0.0001, `${label}: rubric weights must total 1`)
|
||||
check(
|
||||
dimensions.every((item) => Boolean(item.name?.trim()) && Boolean(item.description?.trim())),
|
||||
`${label}: rubric name or description is missing`,
|
||||
)
|
||||
}
|
||||
|
||||
const categoryCounts = new Map()
|
||||
for (const question of content.policyQuestionCandidates ?? []) {
|
||||
const label = question.id ?? 'unknown question'
|
||||
categoryCounts.set(question.category, (categoryCounts.get(question.category) ?? 0) + 1)
|
||||
check(question.status === 'PENDING_REVIEW', `${label}: status must remain pending review`)
|
||||
check(Boolean(question.question?.trim()), `${label}: question is missing`)
|
||||
check(Boolean(question.sourceNeed?.trim()), `${label}: source requirement is missing`)
|
||||
check(Boolean(question.expectedBehavior?.trim()), `${label}: expected boundary behavior is missing`)
|
||||
check(Boolean(question.reviewerRole?.trim()), `${label}: reviewer role is missing`)
|
||||
check(!Object.hasOwn(question, 'answer'), `${label}: answer must not exist before formal sourcing`)
|
||||
check(!Object.hasOwn(question, 'standardAnswer'), `${label}: standardAnswer must not exist before formal sourcing`)
|
||||
}
|
||||
for (const [category, expected] of requiredCategories) {
|
||||
check(categoryCounts.get(category) === expected, `${category}: expected ${expected} questions`)
|
||||
}
|
||||
|
||||
const serialized = JSON.stringify(content)
|
||||
check(!phonePattern.test(serialized), 'candidate pack contains a mobile phone number')
|
||||
check(!preciseRoomPattern.test(serialized), 'candidate pack contains a precise building or room locator')
|
||||
check(!credentialPattern.test(serialized), 'candidate pack contains access-credential wording')
|
||||
check(!/"(?:reviewStatus|answer|standardAnswer)"\s*:/.test(serialized), 'candidate pack contains a publish state or answer field')
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error(`Aug 1 content candidate verification failed (${failures.length}):`)
|
||||
for (const failure of failures) console.error(`- ${failure}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('Aug 1 content candidate verification passed')
|
||||
console.log(`- contentVersion: ${content.contentVersion}`)
|
||||
console.log(`- scenarios: ${content.scenarios.length} (all disabled and pending review)`)
|
||||
console.log(`- questions: ${content.policyQuestionCandidates.length} (6 per category)`)
|
||||
console.log('- formal publishable scenarios: 0')
|
||||
console.log('- formally sourced standard answers: 0')
|
||||
Reference in New Issue
Block a user