fix(release): make August 1 handoff reproducible
This commit is contained in:
+1
-1
@@ -373,7 +373,7 @@ Android 人工验收每完成一个步骤,用 `.\scripts\capture-android-accep
|
|||||||
-BackendJar .\backend\ruoyi-admin\target\ruoyi-admin.jar
|
-BackendJar .\backend\ruoyi-admin\target\ruoyi-admin.jar
|
||||||
```
|
```
|
||||||
|
|
||||||
打包器重新校验冻结证据、APK/JAR/内容哈希和提交祖先关系,按固定顺序与时间戳生成 14 个文件的 ZIP,并附带发布、预检、API 探针、Android 取证及最新 Go/No-Go 的审计快照。ZIP 内 operations 脚本用于交付审计;实际执行仍须从对应 operations commit 的干净仓库运行。
|
打包器重新校验冻结证据、APK/JAR/内容哈希和提交祖先关系,按固定顺序与时间戳生成 17 个文件的 ZIP,并附带发布、预检、无登录 API 探针、固定码认证复验、内容候选校验、业务与五通道签认单、Android 取证及最新 Go/No-Go 的审计快照。ZIP 内 operations 脚本用于交付审计;实际执行仍须从对应 operations commit 的干净仓库运行。固定码认证脚本只有显式传入 `--execute` 才运行,沿用生产现有固定码配置,不启用或发送真实短信,也不提交业务记录。
|
||||||
|
|
||||||
## MVP 页面验证
|
## MVP 页面验证
|
||||||
|
|
||||||
|
|||||||
@@ -122,12 +122,30 @@ $goNoGoCandidates = @(Get-ChildItem -LiteralPath (Join-Path $projectRoot 'docs')
|
|||||||
if ($goNoGoCandidates.Count -ne 1) {
|
if ($goNoGoCandidates.Count -ne 1) {
|
||||||
throw "Expected one Android Go/No-Go document, found $($goNoGoCandidates.Count)."
|
throw "Expected one Android Go/No-Go document, found $($goNoGoCandidates.Count)."
|
||||||
}
|
}
|
||||||
|
$businessSignoffCandidates = @(
|
||||||
|
Get-ChildItem -LiteralPath (Join-Path $projectRoot 'docs') -File -Filter '*20260729.md' |
|
||||||
|
Where-Object {
|
||||||
|
$candidateText = Get-Content -Raw -Encoding UTF8 -LiteralPath $_.FullName
|
||||||
|
$candidateText.Contains('direct_president') -and
|
||||||
|
$candidateText.Contains('direct_finance') -and
|
||||||
|
$candidateText.Contains('direct_hr') -and
|
||||||
|
$candidateText.Contains('direct_audit') -and
|
||||||
|
$candidateText.Contains('direct_operations')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if ($businessSignoffCandidates.Count -ne 1) {
|
||||||
|
throw "Expected one August 1 business-content and five-channel sign-off document, found $($businessSignoffCandidates.Count)."
|
||||||
|
}
|
||||||
|
$businessSignoffPath = $businessSignoffCandidates[0].FullName
|
||||||
$operationSources = [ordered]@{
|
$operationSources = [ordered]@{
|
||||||
'operations/release-backend.sh' = Join-Path $projectRoot 'scripts\release-backend.sh'
|
'operations/release-backend.sh' = Join-Path $projectRoot 'scripts\release-backend.sh'
|
||||||
'operations/release-preflight.sh' = Join-Path $projectRoot 'scripts\release-preflight.sh'
|
'operations/release-preflight.sh' = Join-Path $projectRoot 'scripts\release-preflight.sh'
|
||||||
'operations/verify-aug1-production-api-readonly.sh' = Join-Path $projectRoot 'scripts\verify-aug1-production-api-readonly.sh'
|
'operations/verify-aug1-production-api-readonly.sh' = Join-Path $projectRoot 'scripts\verify-aug1-production-api-readonly.sh'
|
||||||
|
'operations/verify-aug1-authenticated-production.sh' = Join-Path $projectRoot 'scripts\verify-aug1-authenticated-production.sh'
|
||||||
|
'operations/verify-aug1-content-candidates.mjs' = Join-Path $projectRoot 'scripts\verify-aug1-content-candidates.mjs'
|
||||||
'operations/capture-android-acceptance.ps1' = Join-Path $projectRoot 'scripts\capture-android-acceptance.ps1'
|
'operations/capture-android-acceptance.ps1' = Join-Path $projectRoot 'scripts\capture-android-acceptance.ps1'
|
||||||
'operations/go-no-go.md' = $goNoGoCandidates[0].FullName
|
'operations/go-no-go.md' = $goNoGoCandidates[0].FullName
|
||||||
|
'operations/business-content-and-five-channel-signoff.md' = $businessSignoffPath
|
||||||
}
|
}
|
||||||
foreach ($source in $operationSources.Values) {
|
foreach ($source in $operationSources.Values) {
|
||||||
Require-File $source
|
Require-File $source
|
||||||
@@ -147,7 +165,7 @@ Write-Output "apk_sha256=$apkSha256"
|
|||||||
Write-Output "backend_sha256=$backendSha256"
|
Write-Output "backend_sha256=$backendSha256"
|
||||||
Write-Output "content_sha256=$contentSha256"
|
Write-Output "content_sha256=$contentSha256"
|
||||||
Write-Output "package_path=$packagePath"
|
Write-Output "package_path=$packagePath"
|
||||||
Write-Output 'package_entries=14'
|
Write-Output 'package_entries=17'
|
||||||
if ($PlanOnly) {
|
if ($PlanOnly) {
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
@@ -207,6 +225,10 @@ try {
|
|||||||
'',
|
'',
|
||||||
'Run the GET-only API probe before and after release, and capture read-only Android evidence after each manual step. Neither replaces Android device acceptance or formal business sign-off.',
|
'Run the GET-only API probe before and after release, and capture read-only Android evidence after each manual step. Neither replaces Android device acceptance or formal business sign-off.',
|
||||||
'',
|
'',
|
||||||
|
'Recheck the bundled disabled content candidates with `node operations/verify-aug1-content-candidates.mjs aug1-life-advisor-content-candidates-v0.1.json`. Complete `operations/business-content-and-five-channel-signoff.md` before enabling content or binding handlers.',
|
||||||
|
'',
|
||||||
|
'The fixed-code authenticated smoke is bundled as `operations/verify-aug1-authenticated-production.sh`. It requires the explicit `--execute` flag, selects an existing eligible APP identity without printing it, never enables or sends real SMS, and does not submit business records.',
|
||||||
|
'',
|
||||||
'This package does not authorize production deployment, service restart, database change, Git push or public distribution.'
|
'This package does not authorize production deployment, service restart, database change, Git push or public distribution.'
|
||||||
) -join [Environment]::NewLine
|
) -join [Environment]::NewLine
|
||||||
Write-Utf8 -Path (Join-Path $stagingDirectory 'README.md') -Content ($readme + [Environment]::NewLine)
|
Write-Utf8 -Path (Join-Path $stagingDirectory 'README.md') -Content ($readme + [Environment]::NewLine)
|
||||||
@@ -238,7 +260,9 @@ try {
|
|||||||
'- Formally publishable scenarios: 0',
|
'- Formally publishable scenarios: 0',
|
||||||
'- Formally sourced standard answers: 0',
|
'- Formally sourced standard answers: 0',
|
||||||
'',
|
'',
|
||||||
'Candidates are for business review only. Do not import, enable or count them toward August 1 acceptance until formal sources and item-level sign-off are complete.'
|
'Candidates are for business review only. Do not import, enable or count them toward August 1 acceptance until formal sources and item-level sign-off are complete.',
|
||||||
|
'',
|
||||||
|
'Run `node operations/verify-aug1-content-candidates.mjs aug1-life-advisor-content-candidates-v0.1.json` after extraction, then use `operations/business-content-and-five-channel-signoff.md` for item-level review and handler assignment.'
|
||||||
) -join [Environment]::NewLine
|
) -join [Environment]::NewLine
|
||||||
Write-Utf8 -Path (Join-Path $stagingDirectory 'content-review-readme.md') -Content ($contentReview + [Environment]::NewLine)
|
Write-Utf8 -Path (Join-Path $stagingDirectory 'content-review-readme.md') -Content ($contentReview + [Environment]::NewLine)
|
||||||
|
|
||||||
@@ -307,8 +331,8 @@ try {
|
|||||||
|
|
||||||
$allFiles = Get-ChildItem -LiteralPath $stagingDirectory -Recurse -File |
|
$allFiles = Get-ChildItem -LiteralPath $stagingDirectory -Recurse -File |
|
||||||
Sort-Object { $_.FullName.Substring($stagingDirectory.Length + 1) }
|
Sort-Object { $_.FullName.Substring($stagingDirectory.Length + 1) }
|
||||||
if ($allFiles.Count -ne 14) {
|
if ($allFiles.Count -ne 17) {
|
||||||
throw "Release package expected 14 files, found $($allFiles.Count)."
|
throw "Release package expected 17 files, found $($allFiles.Count)."
|
||||||
}
|
}
|
||||||
foreach ($file in $allFiles) {
|
foreach ($file in $allFiles) {
|
||||||
$file.LastWriteTimeUtc = $entryTimestamp.UtcDateTime
|
$file.LastWriteTimeUtc = $entryTimestamp.UtcDateTime
|
||||||
@@ -345,11 +369,20 @@ try {
|
|||||||
|
|
||||||
$verificationArchive = [IO.Compression.ZipFile]::OpenRead($partialPackagePath)
|
$verificationArchive = [IO.Compression.ZipFile]::OpenRead($partialPackagePath)
|
||||||
try {
|
try {
|
||||||
if ($verificationArchive.Entries.Count -ne 14) {
|
if ($verificationArchive.Entries.Count -ne 17) {
|
||||||
throw "ZIP verification expected 14 entries, found $($verificationArchive.Entries.Count)."
|
throw "ZIP verification expected 17 entries, found $($verificationArchive.Entries.Count)."
|
||||||
}
|
}
|
||||||
$entryNames = @($verificationArchive.Entries | ForEach-Object { $_.FullName })
|
$entryNames = @($verificationArchive.Entries | ForEach-Object { $_.FullName })
|
||||||
foreach ($requiredEntry in @($apkName, $backendName, 'release-manifest.json', 'SHA256SUMS.txt', 'operations/release-backend.sh')) {
|
foreach ($requiredEntry in @(
|
||||||
|
$apkName,
|
||||||
|
$backendName,
|
||||||
|
'release-manifest.json',
|
||||||
|
'SHA256SUMS.txt',
|
||||||
|
'operations/release-backend.sh',
|
||||||
|
'operations/verify-aug1-authenticated-production.sh',
|
||||||
|
'operations/verify-aug1-content-candidates.mjs',
|
||||||
|
'operations/business-content-and-five-channel-signoff.md'
|
||||||
|
)) {
|
||||||
if ($entryNames -notcontains $requiredEntry) {
|
if ($entryNames -notcontains $requiredEntry) {
|
||||||
throw "ZIP verification is missing $requiredEntry"
|
throw "ZIP verification is missing $requiredEntry"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||||
|
import os from 'node:os'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { spawnSync } from 'node:child_process'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
const testDir = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const projectRoot = path.resolve(testDir, '..', '..')
|
||||||
|
const verifierPath = path.join(projectRoot, 'scripts', 'verify-aug1-content-candidates.mjs')
|
||||||
|
const candidatePath = path.join(
|
||||||
|
projectRoot,
|
||||||
|
'docs',
|
||||||
|
'content-candidates',
|
||||||
|
'aug1-life-advisor-content-candidates-v0.1.json',
|
||||||
|
)
|
||||||
|
const baseCandidate = JSON.parse(await readFile(candidatePath, 'utf8'))
|
||||||
|
|
||||||
|
const cloneCandidate = () => structuredClone(baseCandidate)
|
||||||
|
|
||||||
|
async function verifyCandidate(candidate) {
|
||||||
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'aug1-content-'))
|
||||||
|
const fixturePath = path.join(tempDir, 'candidate.json')
|
||||||
|
try {
|
||||||
|
await writeFile(fixturePath, JSON.stringify(candidate), 'utf8')
|
||||||
|
return spawnSync(process.execPath, [verifierPath, fixturePath], {
|
||||||
|
cwd: projectRoot,
|
||||||
|
encoding: 'utf8',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
await rm(tempDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('accepts the reviewed disabled candidate structure through an explicit package path', async () => {
|
||||||
|
const result = await verifyCandidate(cloneCandidate())
|
||||||
|
assert.equal(result.status, 0, result.stderr)
|
||||||
|
assert.match(result.stdout, /verification passed/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('rejects sensitive locators, credentials and mobile numbers', async (t) => {
|
||||||
|
const cases = [
|
||||||
|
['precise room', (candidate) => {
|
||||||
|
candidate.scenarios[0].scenarioDraft.openingText = '住户住在3栋2单元501室。'
|
||||||
|
}],
|
||||||
|
['access credential', (candidate) => {
|
||||||
|
candidate.scenarios[0].scenarioDraft.openingText = '门禁口令 ABCD。'
|
||||||
|
}],
|
||||||
|
['mobile phone', (candidate) => {
|
||||||
|
candidate.policyQuestionCandidates[0].question = ['联系号码', '13', '9000', '00000'].join('')
|
||||||
|
}],
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const [name, mutate] of cases) {
|
||||||
|
await t.test(name, async () => {
|
||||||
|
const candidate = cloneCandidate()
|
||||||
|
mutate(candidate)
|
||||||
|
const result = await verifyCandidate(candidate)
|
||||||
|
assert.notEqual(result.status, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('rejects publishable content, answers and duplicate IDs before formal sign-off', async (t) => {
|
||||||
|
const cases = [
|
||||||
|
['publishable root', (candidate) => {
|
||||||
|
candidate.publishable = true
|
||||||
|
}],
|
||||||
|
['embedded answer', (candidate) => {
|
||||||
|
candidate.policyQuestionCandidates[0].answer = 'not allowed before formal sourcing'
|
||||||
|
}],
|
||||||
|
['duplicate scenario ID', (candidate) => {
|
||||||
|
candidate.scenarios[1].candidateId = candidate.scenarios[0].candidateId
|
||||||
|
}],
|
||||||
|
['duplicate question ID', (candidate) => {
|
||||||
|
candidate.policyQuestionCandidates[1].id = candidate.policyQuestionCandidates[0].id
|
||||||
|
}],
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const [name, mutate] of cases) {
|
||||||
|
await t.test(name, async () => {
|
||||||
|
const candidate = cloneCandidate()
|
||||||
|
mutate(candidate)
|
||||||
|
const result = await verifyCandidate(candidate)
|
||||||
|
assert.notEqual(result.status, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -15,10 +15,14 @@ $requiredFragments = @(
|
|||||||
"'operations/release-backend.sh'",
|
"'operations/release-backend.sh'",
|
||||||
"'operations/release-preflight.sh'",
|
"'operations/release-preflight.sh'",
|
||||||
"'operations/verify-aug1-production-api-readonly.sh'",
|
"'operations/verify-aug1-production-api-readonly.sh'",
|
||||||
|
"'operations/verify-aug1-authenticated-production.sh'",
|
||||||
|
"'operations/verify-aug1-content-candidates.mjs'",
|
||||||
"'operations/capture-android-acceptance.ps1'",
|
"'operations/capture-android-acceptance.ps1'",
|
||||||
"'operations/go-no-go.md'",
|
"'operations/go-no-go.md'",
|
||||||
|
"'operations/business-content-and-five-channel-signoff.md'",
|
||||||
'Release package already exists and will not be overwritten',
|
'Release package already exists and will not be overwritten',
|
||||||
'Release package expected 14 files',
|
'Release package expected 17 files',
|
||||||
|
'ZIP verification expected 17 entries',
|
||||||
'ZIP entry hash mismatch',
|
'ZIP entry hash mismatch',
|
||||||
'package_verified=true',
|
'package_verified=true',
|
||||||
'authenticatedFixedCodeSmokePassed = $true',
|
'authenticatedFixedCodeSmokePassed = $true',
|
||||||
|
|||||||
@@ -4,12 +4,19 @@ import path from 'node:path'
|
|||||||
|
|
||||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
||||||
const rootDir = path.resolve(scriptDir, '..')
|
const rootDir = path.resolve(scriptDir, '..')
|
||||||
const candidatePath = path.join(
|
const defaultCandidatePath = path.join(
|
||||||
rootDir,
|
rootDir,
|
||||||
'docs',
|
'docs',
|
||||||
'content-candidates',
|
'content-candidates',
|
||||||
'aug1-life-advisor-content-candidates-v0.1.json',
|
'aug1-life-advisor-content-candidates-v0.1.json',
|
||||||
)
|
)
|
||||||
|
if (process.argv.length > 3) {
|
||||||
|
console.error('Usage: node verify-aug1-content-candidates.mjs [candidate-json]')
|
||||||
|
process.exit(2)
|
||||||
|
}
|
||||||
|
const candidatePath = process.argv[2]
|
||||||
|
? path.resolve(process.argv[2])
|
||||||
|
: defaultCandidatePath
|
||||||
const content = JSON.parse(await readFile(candidatePath, 'utf8'))
|
const content = JSON.parse(await readFile(candidatePath, 'utf8'))
|
||||||
|
|
||||||
const failures = []
|
const failures = []
|
||||||
@@ -38,12 +45,14 @@ const credentialPattern = /(?:门禁|家门|开门|访问)\s*(?:密码|口令|
|
|||||||
|
|
||||||
check(content.candidateStatus === 'PENDING_BUSINESS_REVIEW', 'root status must remain pending review')
|
check(content.candidateStatus === 'PENDING_BUSINESS_REVIEW', 'root status must remain pending review')
|
||||||
check(content.publishable === false, 'root publishable must be false')
|
check(content.publishable === false, 'root publishable must be false')
|
||||||
|
check(Boolean(content.contentVersion?.trim()), 'content version is required')
|
||||||
check(Array.isArray(content.scenarios) && content.scenarios.length === 5, 'exactly five scenario candidates are required')
|
check(Array.isArray(content.scenarios) && content.scenarios.length === 5, 'exactly five scenario candidates are required')
|
||||||
check(
|
check(
|
||||||
Array.isArray(content.policyQuestionCandidates) && content.policyQuestionCandidates.length === 30,
|
Array.isArray(content.policyQuestionCandidates) && content.policyQuestionCandidates.length === 30,
|
||||||
'exactly thirty policy question candidates are required',
|
'exactly thirty policy question candidates are required',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const scenarioIds = new Set()
|
||||||
for (const scenario of content.scenarios ?? []) {
|
for (const scenario of content.scenarios ?? []) {
|
||||||
const label = scenario.candidateId ?? 'unknown scenario'
|
const label = scenario.candidateId ?? 'unknown scenario'
|
||||||
const draft = scenario.scenarioDraft ?? {}
|
const draft = scenario.scenarioDraft ?? {}
|
||||||
@@ -61,13 +70,20 @@ for (const scenario of content.scenarios ?? []) {
|
|||||||
draft.goal,
|
draft.goal,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
check(Boolean(scenario.candidateId?.trim()), `${label}: candidate ID is required`)
|
||||||
|
check(!scenarioIds.has(scenario.candidateId), `${label}: candidate ID is duplicated`)
|
||||||
|
scenarioIds.add(scenario.candidateId)
|
||||||
check(scenario.candidateStatus === 'PENDING_BUSINESS_REVIEW', `${label}: status must remain pending review`)
|
check(scenario.candidateStatus === 'PENDING_BUSINESS_REVIEW', `${label}: status must remain pending review`)
|
||||||
check(scenario.publishable === false, `${label}: publishable must be false`)
|
check(scenario.publishable === false, `${label}: publishable must be false`)
|
||||||
check(draft.enabled === false, `${label}: scenario must be disabled`)
|
check(draft.enabled === false, `${label}: scenario must be disabled`)
|
||||||
check(draft.riskLevel === '待评估', `${label}: risk must remain pending until business review`)
|
check(draft.riskLevel === '待评估', `${label}: risk must remain pending until business review`)
|
||||||
check(rubric.enabled === false, `${label}: rubric must be disabled`)
|
check(rubric.enabled === false, `${label}: rubric must be disabled`)
|
||||||
check(placeholderPattern.test(draft.sopRefs ?? ''), `${label}: missing-source marker must remain explicit`)
|
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.sourceRefs ?? []).length >= 2
|
||||||
|
&& scenario.sourceRefs.every((item) => typeof item === 'string' && item.trim()),
|
||||||
|
`${label}: at least two source references are required`,
|
||||||
|
)
|
||||||
check((scenario.requiredReviewers ?? []).length >= 2, `${label}: at least two reviewer roles 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((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(requiredText.every((value) => typeof value === 'string' && value.trim()), `${label}: required scenario fields are incomplete`)
|
||||||
@@ -93,14 +109,22 @@ for (const scenario of content.scenarios ?? []) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const categoryCounts = new Map()
|
const categoryCounts = new Map()
|
||||||
|
const questionIds = new Set()
|
||||||
for (const question of content.policyQuestionCandidates ?? []) {
|
for (const question of content.policyQuestionCandidates ?? []) {
|
||||||
const label = question.id ?? 'unknown question'
|
const label = question.id ?? 'unknown question'
|
||||||
categoryCounts.set(question.category, (categoryCounts.get(question.category) ?? 0) + 1)
|
categoryCounts.set(question.category, (categoryCounts.get(question.category) ?? 0) + 1)
|
||||||
|
check(Boolean(question.id?.trim()), `${label}: question ID is required`)
|
||||||
|
check(!questionIds.has(question.id), `${label}: question ID is duplicated`)
|
||||||
|
questionIds.add(question.id)
|
||||||
check(question.status === 'PENDING_REVIEW', `${label}: status must remain pending review`)
|
check(question.status === 'PENDING_REVIEW', `${label}: status must remain pending review`)
|
||||||
check(Boolean(question.question?.trim()), `${label}: question is missing`)
|
check(Boolean(question.question?.trim()), `${label}: question is missing`)
|
||||||
check(Boolean(question.sourceNeed?.trim()), `${label}: source requirement 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.expectedBehavior?.trim()), `${label}: expected boundary behavior is missing`)
|
||||||
check(Boolean(question.reviewerRole?.trim()), `${label}: reviewer role is missing`)
|
check(Boolean(question.reviewerRole?.trim()), `${label}: reviewer role is missing`)
|
||||||
|
check(
|
||||||
|
['NEEDS_FORMAL_SOURCE', 'BOUNDARY_EXPECTATION_DEFINED'].includes(question.answerStatus),
|
||||||
|
`${label}: answer status is invalid`,
|
||||||
|
)
|
||||||
check(!Object.hasOwn(question, 'answer'), `${label}: answer must not exist before formal sourcing`)
|
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`)
|
check(!Object.hasOwn(question, 'standardAnswer'), `${label}: standardAnswer must not exist before formal sourcing`)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user