Files
prop-ai-hr/scripts/verify-aug1-formal-content.mjs
T

353 lines
16 KiB
JavaScript

import { createHash } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.resolve(scriptDir, '..')
const defaultApprovalPath = path.join(
rootDir,
'docs',
'content-candidates',
'aug1-release-approval.json',
)
let strict = false
let scenarioSnapshots = false
const positional = []
for (const argument of process.argv.slice(2)) {
if (argument === '--strict') {
strict = true
} else if (argument === '--scenario-snapshots') {
strict = true
scenarioSnapshots = true
} else if (argument.startsWith('-')) {
console.error(`Unknown option: ${argument}`)
process.exit(2)
} else {
positional.push(argument)
}
}
if (positional.length > 1) {
console.error('Usage: node verify-aug1-formal-content.mjs [--strict] [--scenario-snapshots] [approval-json]')
process.exit(2)
}
const approvalPath = positional[0] ? path.resolve(positional[0]) : defaultApprovalPath
const approval = JSON.parse(await readFile(approvalPath, 'utf8'))
const failures = []
const check = (condition, message) => {
if (!condition) failures.push(message)
}
const nonBlank = (value) => typeof value === 'string' && value.trim().length > 0
const sha256Pattern = /^[0-9a-f]{64}$/
const reviewedAtPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|[+-]\d{2}:\d{2})$/
const sourceIdPattern = /^[A-Z0-9][A-Z0-9._-]{1,79}$/
const scenarioCodePattern = /^[a-z0-9][a-z0-9-]{1,79}$/
const placeholderPattern = /(?:待补|待定|待填写|待签|TBD|TODO)/i
check(approval.schemaVersion === '1.0', 'approval schemaVersion must be 1.0')
check(approval.releaseTarget === '2026-08-01', 'release target must be 2026-08-01')
check(/^[0-9A-Za-z_-]{1,20}$/.test(approval.tenantId ?? ''), 'tenantId is invalid')
const candidateFileValid = nonBlank(approval.candidateFile)
&& path.basename(approval.candidateFile) === approval.candidateFile
&& approval.candidateFile.endsWith('.json')
check(candidateFileValid, 'candidateFile must be a JSON basename beside the approval file')
check(sha256Pattern.test(approval.candidateSha256 ?? ''), 'candidateSha256 must be lowercase SHA-256')
check(
sha256Pattern.test(approval.expectedApkSignerSha256 ?? ''),
'expectedApkSignerSha256 must be lowercase SHA-256',
)
if (!candidateFileValid) {
for (const failure of failures) console.error(`FAIL: ${failure}`)
process.exit(1)
}
const candidatePath = path.resolve(path.dirname(approvalPath), approval.candidateFile ?? '')
let candidateBytes
try {
candidateBytes = await readFile(candidatePath)
} catch {
console.error('FAIL: candidate file cannot be read beside the approval manifest')
process.exit(1)
}
const candidateHash = createHash('sha256').update(candidateBytes).digest('hex')
check(candidateHash === approval.candidateSha256, 'candidate file SHA-256 does not match approval manifest')
const candidate = JSON.parse(candidateBytes.toString('utf8'))
check(candidate.candidateStatus === 'PENDING_BUSINESS_REVIEW', 'candidate source must remain pending review')
check(candidate.publishable === false, 'candidate source must remain non-publishable')
check(Array.isArray(candidate.scenarios) && candidate.scenarios.length === 5, 'candidate source must contain five scenarios')
check(
Array.isArray(candidate.policyQuestionCandidates) && candidate.policyQuestionCandidates.length === 30,
'candidate source must contain thirty policy questions',
)
const expectedScenarios = new Map()
for (const item of candidate.scenarios ?? []) {
check(nonBlank(item.candidateId), 'candidate scenario ID is required')
check(nonBlank(item.scenarioDraft?.id), `${item.candidateId ?? 'unknown scenario'}: scenario code is required`)
check(!expectedScenarios.has(item.candidateId), `${item.candidateId}: duplicate candidate scenario ID`)
expectedScenarios.set(item.candidateId, item)
}
const expectedQuestions = new Map()
for (const item of candidate.policyQuestionCandidates ?? []) {
check(nonBlank(item.id), 'candidate question ID is required')
check(!expectedQuestions.has(item.id), `${item.id}: duplicate candidate question ID`)
expectedQuestions.set(item.id, item)
}
const sourceRegistry = Array.isArray(approval.sourceRegistry) ? approval.sourceRegistry : []
check(Array.isArray(approval.sourceRegistry), 'sourceRegistry must be an array')
const sourceIds = new Set()
for (const source of sourceRegistry) {
const label = source.sourceId ?? 'unknown source'
check(sourceIdPattern.test(source.sourceId ?? ''), `${label}: sourceId is invalid`)
check(!sourceIds.has(source.sourceId), `${label}: duplicate sourceId`)
sourceIds.add(source.sourceId)
check(nonBlank(source.title) && !placeholderPattern.test(source.title), `${label}: formal source title is required`)
check(nonBlank(source.version) && !placeholderPattern.test(source.version), `${label}: formal source version is required`)
check(/^\d{4}-\d{2}-\d{2}$/.test(source.effectiveDate ?? ''), `${label}: effectiveDate must be YYYY-MM-DD`)
check(nonBlank(source.scope) && !placeholderPattern.test(source.scope), `${label}: source scope is required`)
check(sha256Pattern.test(source.sha256 ?? ''), `${label}: source SHA-256 is required`)
check(source.formalPolicy === true, `${label}: source must be marked formalPolicy=true`)
}
const scenarioApprovals = approval.scenarioApprovals
const questionApprovals = approval.policyQuestionApprovals
const channelApprovals = approval.channelApprovals
check(
scenarioApprovals && typeof scenarioApprovals === 'object' && !Array.isArray(scenarioApprovals),
'scenarioApprovals must be an object keyed by candidate ID',
)
check(
questionApprovals && typeof questionApprovals === 'object' && !Array.isArray(questionApprovals),
'policyQuestionApprovals must be an object keyed by question ID',
)
check(
channelApprovals && typeof channelApprovals === 'object' && !Array.isArray(channelApprovals),
'channelApprovals must be an object keyed by direct-channel role',
)
const scenarioEntries = Object.entries(scenarioApprovals ?? {})
const questionEntries = Object.entries(questionApprovals ?? {})
const channelEntries = Object.entries(channelApprovals ?? {})
for (const [candidateId] of scenarioEntries) {
check(expectedScenarios.has(candidateId), `${candidateId}: approval references an unknown scenario`)
}
for (const [questionId] of questionEntries) {
check(expectedQuestions.has(questionId), `${questionId}: approval references an unknown question`)
}
const requiredChannels = [
'direct_president',
'direct_finance',
'direct_hr',
'direct_audit',
'direct_operations',
]
for (const [roleKey] of channelEntries) {
check(requiredChannels.includes(roleKey), `${roleKey}: approval references an unknown direct channel`)
}
const resolveSourceRefs = (refs, label, required = true) => {
check(Array.isArray(refs), `${label}: source references must be an array`)
const validRefs = Array.isArray(refs) ? refs : []
if (required) {
check(validRefs.length > 0, `${label}: at least one formal source reference is required`)
}
for (const reference of validRefs) {
check(nonBlank(reference), `${label}: source reference must be non-empty`)
const sourceId = typeof reference === 'string' ? reference.split(':', 1)[0] : ''
check(sourceIds.has(sourceId), `${label}: source reference ${reference} is not in sourceRegistry`)
}
}
const requireReview = (entry, label, prefix = '') => {
const reviewedBy = prefix ? entry[`${prefix}ReviewedBy`] : entry.reviewedBy
const reviewedAt = prefix ? entry[`${prefix}ReviewedAt`] : entry.reviewedAt
check(nonBlank(reviewedBy) && !placeholderPattern.test(reviewedBy), `${label}: ${prefix || 'first'} reviewer is required`)
check(reviewedAtPattern.test(reviewedAt ?? ''), `${label}: ${prefix || 'first'} review time must include timezone`)
}
if (strict) {
check(approval.releaseStatus === 'APPROVED', 'releaseStatus must be APPROVED')
check(
nonBlank(approval.formalContentVersion) && !placeholderPattern.test(approval.formalContentVersion),
'formalContentVersion is required',
)
check(sourceRegistry.length > 0, 'at least one formal source is required')
check(scenarioEntries.length === expectedScenarios.size, 'scenario approvals must cover exactly 5/5 candidates')
check(questionEntries.length === expectedQuestions.size, 'question approvals must cover exactly 30/30 candidates')
for (const [candidateId, candidateItem] of expectedScenarios) {
const entry = scenarioApprovals?.[candidateId]
const label = candidateId
check(Boolean(entry), `${label}: scenario approval is missing`)
if (!entry) continue
check(entry.status === 'APPROVED', `${label}: scenario status must be APPROVED`)
check(entry.scenarioCode === candidateItem.scenarioDraft.id, `${label}: production scenario code does not match candidate`)
check(scenarioCodePattern.test(entry.scenarioCode ?? ''), `${label}: production scenario code is invalid`)
check(
['常规', '高风险'].includes(entry.riskLevel) && entry.riskLevel === candidateItem.proposedRiskLevel,
`${label}: risk level must match the reviewed candidate proposal`,
)
resolveSourceRefs(entry.sourceRefs, label)
requireReview(entry, label)
if (entry.riskLevel === '高风险') {
requireReview(entry, label, 'second')
check(entry.secondReviewedBy !== entry.reviewedBy, `${label}: high-risk reviewers must be different people`)
}
check(nonBlank(entry.businessEvidenceRef), `${label}: business evidence reference is required`)
check(sha256Pattern.test(entry.businessEvidenceSha256 ?? ''), `${label}: business evidence SHA-256 is required`)
check(nonBlank(entry.productionContentVersion), `${label}: production content version is required`)
check(sha256Pattern.test(entry.productionContentHash ?? ''), `${label}: production content hash is required`)
}
const dispositions = new Set([
'FORMALLY_SOURCED',
'FORMAL_CONFLICT_BOUNDARY',
'FORMAL_NO_EVIDENCE_BOUNDARY',
'FORMAL_UNAUTHORIZED_BOUNDARY',
])
for (const [questionId, candidateItem] of expectedQuestions) {
const entry = questionApprovals?.[questionId]
const label = questionId
check(Boolean(entry), `${label}: question approval is missing`)
if (!entry) continue
check(entry.status === 'APPROVED', `${label}: question status must be APPROVED`)
check(dispositions.has(entry.answerDisposition), `${label}: answerDisposition is invalid`)
if (candidateItem.category === 'CONFLICT') {
check(
entry.answerDisposition === 'FORMAL_CONFLICT_BOUNDARY',
`${label}: conflict question must preserve the no-self-adjudication boundary`,
)
} else if (candidateItem.category === 'NO_EVIDENCE') {
check(
entry.answerDisposition === 'FORMAL_NO_EVIDENCE_BOUNDARY',
`${label}: no-evidence question must preserve the formal refusal boundary`,
)
} else if (candidateItem.category === 'UNAUTHORIZED') {
check(
entry.answerDisposition === 'FORMAL_UNAUTHORIZED_BOUNDARY',
`${label}: unauthorized question must preserve the permission boundary`,
)
} else {
check(
['FORMALLY_SOURCED', 'FORMAL_NO_EVIDENCE_BOUNDARY'].includes(entry.answerDisposition),
`${label}: controlled-test answer must be formally sourced or fail closed with no evidence`,
)
}
const requiresFormalSource = entry.answerDisposition === 'FORMALLY_SOURCED'
resolveSourceRefs(entry.sourceRefs, label, requiresFormalSource)
if (!requiresFormalSource) {
check(
/(?:CONFLICT|NO_EVIDENCE|UNAUTHORIZED)_BOUNDARY_DETECTED/.test(entry.observedBehavior ?? ''),
`${label}: fail-closed disposition requires matching observed boundary evidence`,
)
}
check(nonBlank(entry.expectedBehavior), `${label}: expected behavior is required`)
check(nonBlank(entry.observedBehavior), `${label}: observed behavior is required`)
check(nonBlank(entry.evidenceRef), `${label}: acceptance evidence reference is required`)
check(sha256Pattern.test(entry.evidenceSha256 ?? ''), `${label}: acceptance evidence SHA-256 is required`)
requireReview(entry, label)
}
check(channelEntries.length === requiredChannels.length, 'channel approvals must cover exactly 5/5 roles')
for (const roleKey of requiredChannels) {
const entry = channelApprovals?.[roleKey]
const label = roleKey
check(Boolean(entry), `${label}: channel approval is missing`)
if (!entry) continue
check(entry.status === 'APPROVED', `${label}: channel status must be APPROVED`)
check(entry.minimumActiveHandlers === 2, `${label}: primary and backup requirement must remain 2`)
for (const evidenceName of [
'bindingEvidence',
'positiveAccessEvidence',
'crossChannelDenialEvidence',
'singleReplyEvidence',
]) {
check(nonBlank(entry[`${evidenceName}Ref`]), `${label}: ${evidenceName} reference is required`)
check(
sha256Pattern.test(entry[`${evidenceName}Sha256`] ?? ''),
`${label}: ${evidenceName} SHA-256 is required`,
)
}
requireReview(entry, label)
}
const requiredSignoffs = [
'businessOwner',
'knowledgeOwner',
'trainingOwner',
'channelOwner',
'releaseOwner',
]
check(
approval.signoffs && typeof approval.signoffs === 'object' && !Array.isArray(approval.signoffs),
'signoffs must be an object',
)
for (const role of requiredSignoffs) {
const signoff = approval.signoffs?.[role]
check(signoff?.status === 'APPROVED', `${role}: sign-off must be APPROVED`)
if (signoff) requireReview(signoff, role)
}
const distribution = approval.distributionApproval
check(
distribution && typeof distribution === 'object' && !Array.isArray(distribution),
'distributionApproval must be an object',
)
if (distribution) {
check(distribution.status === 'APPROVED', 'distribution approval must be APPROVED')
check(distribution.scope === 'CONTROLLED_INTERNAL_TEST', 'distribution scope must remain controlled internal test')
check(
['LEGAL_APPROVED', 'CONTROLLED_INTERNAL_TEST_EXCEPTION_ACCEPTED'].includes(distribution.legalDecision),
'distribution legalDecision is not approved',
)
check(
[
'ENTERPRISE_SIGNER_APPROVED',
'DCLOUD_TEST_SIGNER_ACCEPTED_FOR_CONTROLLED_INTERNAL_TEST',
].includes(distribution.signerDecision),
'distribution signerDecision is not approved',
)
check(
distribution.signerSha256 === approval.expectedApkSignerSha256,
'distribution signer SHA-256 must match the expected APK signer',
)
check(nonBlank(distribution.distributionEvidenceRef), 'controlled-distribution evidence reference is required')
check(
sha256Pattern.test(distribution.distributionEvidenceSha256 ?? ''),
'controlled-distribution evidence SHA-256 is required',
)
requireReview(distribution, 'distributionApproval')
}
}
if (failures.length > 0) {
for (const failure of failures) console.error(`FAIL: ${failure}`)
process.exit(1)
}
if (scenarioSnapshots) {
for (const [candidateId, candidateItem] of expectedScenarios) {
const entry = scenarioApprovals[candidateId]
console.log([
candidateItem.scenarioDraft.id,
entry.productionContentVersion,
entry.productionContentHash,
].join('|'))
}
} else {
const approvedScenarios = scenarioEntries.filter(([, value]) => value?.status === 'APPROVED').length
const approvedQuestions = questionEntries.filter(([, value]) => value?.status === 'APPROVED').length
const approvedChannels = channelEntries.filter(([, value]) => value?.status === 'APPROVED').length
const approvedSignoffs = Object.values(approval.signoffs ?? {}).filter((value) => value?.status === 'APPROVED').length
console.log('August 1 formal content approval audit passed')
console.log(`- releaseStatus: ${approval.releaseStatus}`)
console.log(`- formal sources: ${sourceRegistry.length}`)
console.log(`- approved scenarios: ${approvedScenarios}/5`)
console.log(`- approved policy questions: ${approvedQuestions}/30`)
console.log(`- approved direct channels: ${approvedChannels}/5`)
console.log(`- approved owner sign-offs: ${approvedSignoffs}/5`)
console.log(`- controlled distribution: ${approval.distributionApproval?.status ?? 'MISSING'}`)
console.log(`- strict release ready: ${strict ? 'true' : 'not requested'}`)
}