feat: 增加人机共管场景决策树 (#8)

Co-authored-by: yige <yige@yigedeMacBook-Neo.local>
This commit is contained in:
2026-08-30 14:01:19 -04:00
committed by GitHub
parent af338dc13b
commit 3261135305
10 changed files with 1044 additions and 26 deletions

View File

@@ -28,6 +28,9 @@ addFormats(ajv)
const validatePublicReleaseAttestationSchema = ajv.compile(
JSON.parse(readFileSync(join(codeRoot, 'public-release-attestation.schema.json'), 'utf8'))
)
const validateDecisionTreeSchema = ajv.compile(
JSON.parse(readFileSync(join(codeRoot, 'decision-tree.schema.json'), 'utf8'))
)
const outputRoot = resolveSafeOutputRoot({
inputRoot,
@@ -263,6 +266,201 @@ const parseSummaryJson = (event) => {
}
}
const compactList = (items, limit = 3) => {
const visible = items.slice(0, limit)
return `${visible.join('')}${items.length > limit ? `;另有 ${items.length - limit}` : ''}`
}
const observedStatusLabel = (status) =>
({ optimal: '最优解', infeasible: '不可行', pass: '通过', fail: '拒绝' })[status] ?? String(status)
const buildDecisionTree = ({ pack, packSha256, evidence, workflowKind, observed, problemFamily, engineId }) => {
const hardChecks = pack.oracle?.hardChecks ?? []
const intent = {
id: 'intent',
kind: 'context',
owner: 'shared',
title: pack.businessContract.decision,
detail: pack.businessContract.plainLanguageStory,
status: 'neutral',
evidenceRef: '#scenario-and-decision',
}
let nodes
let edges
if (workflowKind === 'optimization') {
const isInfeasible = observed.solveStatus === 'infeasible'
const isRejected = observed.validationVerdict === 'fail'
nodes = [
intent,
{
id: 'facts', kind: 'question', owner: 'human', title: '业务事实、硬规则与优先级是否已确认?',
detail: `${compactList(pack.businessContract.objectives)}${compactList(pack.businessContract.constraints)}`,
status: 'pass', evidenceRef: '#input-and-provenance',
},
{
id: 'clarify', kind: 'action', owner: 'agent', title: '信息不足:向人类补问',
detail: '缺少决定性事实时保持阻断,不猜测参数、不静默补值。', status: 'guard',
evidenceRef: '#input-and-provenance',
},
{
id: 'model', kind: 'check', owner: 'agent',
title: `规则是否完整映射为 ${String(problemFamily).toUpperCase()} 决策模型?`,
detail: hardChecks.length ? `交付必须覆盖 ${compactList(hardChecks, 4)}` : pack.businessContract.professionalSummary,
status: 'pass', evidenceRef: '#model-definition',
},
{
id: 'mapping-block', kind: 'outcome', owner: 'shared', title: '模型映射不完整:停止求解',
detail: '由人类补充或修正规则Agent 不得以近似模型冒充原问题。', status: 'blocked',
evidenceRef: '#model-definition',
},
{
id: 'candidate', kind: 'action', owner: 'agent',
title: isInfeasible ? `使用 ${engineId} 做可行性诊断` : `使用 ${engineId} 生成候选方案`,
detail: `真实求解回执:${observedStatusLabel(observed.solveStatus)}${observed.solveStatus}${pack.businessContract.baseline}`,
status: isInfeasible ? 'guard' : 'pass', evidenceRef: '#solve-process',
},
{
id: 'verify', kind: 'question', owner: 'validator',
title: isInfeasible ? '不可行结论与冲突集合是否可追溯?' : '独立校核是否接受当前候选?',
detail: hardChecks.length ? `重新检查 ${compactList(hardChecks, 4)}` : '重新计算变量域、硬约束、目标与证据哈希。',
status: isRejected ? 'guard' : 'pass', evidenceRef: '#independent-validation',
},
{
id: 'verification-guard', kind: 'outcome', owner: 'validator', title: '校核证据不足:拒绝交付',
detail: '不采信求解器自报;保留候选、失败检查和哈希后返回 Agent 修正。', status: 'blocked',
evidenceRef: '#independent-validation',
},
{
id: 'outcome', kind: 'outcome', owner: 'human',
title: isRejected ? '阻断违规方案,不进入执行' : isInfeasible ? '交付冲突诊断,等待人类调整规则' : '建议方案交由业务负责人确认',
detail: isRejected ? '独立校核拒绝该候选;保留失败证据供人复核。' : isInfeasible ? '不偷偷删除任务、放宽容量或伪造可行解。' : `${pack.businessContract.objectives.join('')};执行前仍由人类确认适用边界。`,
status: isRejected ? 'blocked' : isInfeasible ? 'guard' : 'pass', evidenceRef: '#reproduction-and-conclusion',
},
]
edges = [
{ from: 'intent', to: 'facts', label: '进入事实确认', tone: 'continue', selected: true },
{ from: 'facts', to: 'clarify', label: '否:缺少决定性信息', tone: 'guard', selected: false },
{ from: 'facts', to: 'model', label: '是:事实门关闭', tone: 'continue', selected: true },
{ from: 'model', to: 'mapping-block', label: '否:存在未映射规则', tone: 'guard', selected: false },
{ from: 'model', to: 'candidate', label: '是:模型可执行', tone: 'continue', selected: true },
{ from: 'candidate', to: 'verify', label: `求解回执:${observedStatusLabel(observed.solveStatus)}`, tone: isInfeasible ? 'guard' : 'continue', selected: true },
{ from: 'verify', to: 'verification-guard', label: '证据不足或对账失败', tone: 'guard', selected: false },
{ from: 'verify', to: 'outcome', label: isRejected ? '校核回执:拒绝' : isInfeasible ? '校核回执:通过,冲突可追溯' : '校核回执:通过', tone: isRejected ? 'guard' : 'success', selected: true },
]
} else if (workflowKind === 'validation') {
const isRejected = observed.validationVerdict === 'fail'
nodes = [
intent,
{
id: 'candidate', kind: 'question', owner: 'human', title: '候选变量、目标声明与适用边界是否齐全?',
detail: `${compactList(pack.businessContract.constraints)}${pack.businessContract.baseline}`,
status: 'pass', evidenceRef: '#input-and-provenance',
},
{
id: 'clarify', kind: 'action', owner: 'agent', title: '候选信息不全:请求补充',
detail: '缺少变量、约束、目标口径或来源时不启动校核。', status: 'guard', evidenceRef: '#input-and-provenance',
},
{
id: 'verify', kind: 'check', owner: 'validator', title: '不重新求解,独立复算候选',
detail: hardChecks.length ? `核对 ${compactList(hardChecks, 4)}` : '核对变量域、硬约束、目标与基线。',
status: isRejected ? 'guard' : 'pass', evidenceRef: '#independent-validation',
},
{
id: 'verification-guard', kind: 'outcome', owner: 'validator', title: '校核证据不足:拒绝下结论',
detail: '验证报告或工具回执缺失时保持阻断。', status: 'blocked', evidenceRef: '#independent-validation',
},
{
id: 'outcome', kind: 'outcome', owner: 'human',
title: isRejected ? '阻断违规方案,不进入执行' : '候选通过校核,交由人类决定是否采用',
detail: isRejected ? '高目标值不能覆盖硬约束违规本次独立校核回执为拒绝fail。' : '本次独立校核回执为通过pass验证通过不等于自动批准执行。',
status: isRejected ? 'blocked' : 'pass', evidenceRef: '#reproduction-and-conclusion',
},
]
edges = [
{ from: 'intent', to: 'candidate', label: '进入候选校核', tone: 'continue', selected: true },
{ from: 'candidate', to: 'clarify', label: '否:候选信息不完整', tone: 'guard', selected: false },
{ from: 'candidate', to: 'verify', label: '是:只校核、不重算方案', tone: 'continue', selected: true },
{ from: 'verify', to: 'verification-guard', label: '验证证据缺失', tone: 'guard', selected: false },
{ from: 'verify', to: 'outcome', label: `校核回执:${observedStatusLabel(observed.validationVerdict)}`, tone: isRejected ? 'guard' : 'success', selected: true },
]
} else {
nodes = [
intent,
{
id: 'recovery-source', kind: 'question', owner: 'platform', title: '正常关闭标记与恢复前基线是否可读取?',
detail: pack.businessContract.baseline, status: 'pass', evidenceRef: '#input-and-provenance',
},
{
id: 'missing-state', kind: 'outcome', owner: 'platform', title: '恢复来源不完整:停止声称恢复成功',
detail: '缺少关闭标记、团队快照或账本时,必须进入人工排查。', status: 'blocked', evidenceRef: '#input-and-provenance',
},
{
id: 'restore', kind: 'action', owner: 'platform', title: '恢复团队、Skill、工具与既有证据',
detail: '该路径不建立、不编译、不求解优化模型。', status: 'pass', evidenceRef: '#solve-process',
},
{
id: 'ledger-check', kind: 'check', owner: 'validator', title: '恢复前后 EvidenceLedger 是否保持幂等?',
detail: compactList(pack.businessContract.constraints, 4), status: 'pass', evidenceRef: '#independent-validation',
},
{
id: 'ledger-guard', kind: 'outcome', owner: 'validator', title: '账本计数或证据变化:拒绝恢复结论',
detail: '发现重复已结算调用或身份漂移时保持阻断。', status: 'blocked', evidenceRef: '#independent-validation',
},
{
id: 'outcome', kind: 'outcome', owner: 'human', title: '恢复完成,不重复已结算求解',
detail: '本次 Run 已通过,且 EvidenceLedger 回执已绑定到这棵树。', status: 'pass', evidenceRef: '#reproduction-and-conclusion',
},
]
edges = [
{ from: 'intent', to: 'recovery-source', label: '进入恢复核对', tone: 'continue', selected: true },
{ from: 'recovery-source', to: 'missing-state', label: '否:恢复来源缺失', tone: 'guard', selected: false },
{ from: 'recovery-source', to: 'restore', label: '是:加载既有状态', tone: 'continue', selected: true },
{ from: 'restore', to: 'ledger-check', label: '读取 EvidenceLedger', tone: 'continue', selected: true },
{ from: 'ledger-check', to: 'ledger-guard', label: '否:计数或身份漂移', tone: 'guard', selected: false },
{ from: 'ledger-check', to: 'outcome', label: '是:无重复求解', tone: 'success', selected: true },
]
}
const tree = {
schemaVersion: 'solver.human-agent-decision-tree/v1', rootNodeId: 'intent', nodes, edges, observed,
evidenceBindings: {
scenarioPackSha256: packSha256,
runId: evidence.runId,
sourceMessagesSha256: evidence.sourceMessagesSha256,
sourceSessionSha256: evidence.sourceSessionSha256,
sourceToolInvocationsSha256: evidence.sourceToolInvocationsSha256,
optimizationSpecSha256: evidence.optimizationSpecSha256,
resultPayloadSha256: evidence.resultPayloadSha256,
validationReportSha256: evidence.validationReportSha256,
},
}
if (!validateDecisionTreeSchema(tree)) {
throw new Error(`decision tree schema validation failed: ${ajv.errorsText(validateDecisionTreeSchema.errors)}`)
}
const nodeIds = new Set(tree.nodes.map((node) => node.id))
if (nodeIds.size !== tree.nodes.length || !nodeIds.has(tree.rootNodeId)) throw new Error('decision tree node IDs invalid')
if (tree.edges.some((edge) => !nodeIds.has(edge.from) || !nodeIds.has(edge.to))) {
throw new Error('decision tree edge refers to an unknown node')
}
const selectedEdges = tree.edges.filter((edge) => edge.selected)
const selectedByFrom = new Map()
for (const edge of selectedEdges) selectedByFrom.set(edge.from, [...(selectedByFrom.get(edge.from) ?? []), edge])
if ([...selectedByFrom.values()].some((outgoing) => outgoing.length !== 1)) {
throw new Error('decision tree path must have exactly one selected outgoing edge per path node')
}
if (tree.nodes.some((node) => node.id !== tree.rootNodeId && !tree.edges.some((edge) => edge.to === node.id))) {
throw new Error('decision tree contains an orphan node')
}
let cursor = tree.rootNodeId
const visited = new Set()
while (selectedByFrom.has(cursor) && !visited.has(cursor)) {
visited.add(cursor)
cursor = selectedByFrom.get(cursor)[0].to
}
if (cursor !== 'outcome' || visited.size !== selectedEdges.length || selectedByFrom.has('outcome')) {
throw new Error('decision tree selected path must be acyclic, complete, and terminate at outcome')
}
return tree
}
const buildChapter = async (planItem, order) => {
const source = caseRunById.get(planItem.runId)
if (!source) throw new Error(`${planItem.scenarioId}: accepted Run evidence missing: ${planItem.runId}`)
@@ -305,14 +503,70 @@ const buildChapter = async (planItem, order) => {
const timeline = [...renderedMessages, ...toolEvents].sort(
(left, right) => Number(left.timestamp ?? 0) - Number(right.timestamp ?? 0) || left.kind.localeCompare(right.kind)
)
const validationSummaries = session
.filter((event) => event.type === 'tool_use_summary' && event.data?.metadata?.validation_subject !== undefined)
.map(parseSummaryJson)
.filter(Boolean)
const validation = validationSummaries.at(-1) ?? null
const parsedToolSummaries = session
.filter((event) => event.type === 'tool_use_summary')
.map((event) => ({ event, summary: parseSummaryJson(event) }))
.filter((entry) => entry.summary && typeof entry.summary === 'object' && !Array.isArray(entry.summary))
const finished = invocations.filter((item) => item.phase === 'finished' && item.status === 'success')
const solve = finished.find((item) => item.tool_name === 'OptimizationSolve')
const validate = [...finished].reverse().find((item) => item.tool_name === 'OptimizationValidate')
const ledger = [...finished].reverse().find((item) => item.tool_name === 'OptimizationEvidenceLedger')
const compile = finished.find((item) => item.tool_name === 'OptimizationCompile')
const solveSummary = solve
? typeof solve.result_provenance?.result_payload_hash === 'string'
? parsedToolSummaries.find(
({ summary }) =>
typeof summary.status === 'string' &&
summary.result_payload_hash === solve.result_provenance?.result_payload_hash
)?.summary ?? null
: null
: null
const validationEntry = validate
? typeof validate.result_provenance?.validation_report_hash === 'string'
? [...parsedToolSummaries]
.reverse()
.find(
({ event, summary }) =>
['pass', 'fail'].includes(summary.verdict) &&
event.data?.metadata?.validation_report_hash === validate.result_provenance?.validation_report_hash
) ?? null
: null
: null
const validation = validationEntry?.summary ?? null
const ledgerSummary = ledger
? [...parsedToolSummaries]
.reverse()
.find(({ summary }) => summary.schema_version === 'solver.optimization-evidence-ledger/v1')?.summary ?? null
: null
if (source.data.status !== 'passed') throw new Error(`${planItem.scenarioId}: decision tree requires an accepted Run`)
const workflowKind = solve ? 'optimization' : validate ? 'validation' : ledger ? 'recovery' : null
if (!workflowKind) throw new Error(`${planItem.scenarioId}: no observed solve, validation, or recovery workflow`)
if (workflowKind !== 'optimization' && compile) {
throw new Error(`${planItem.scenarioId}: ${workflowKind} workflow cannot hide an observed OptimizationCompile step`)
}
if (workflowKind === 'optimization' && (!solveSummary || !validation)) {
throw new Error(`${planItem.scenarioId}: settled solve or validation summary missing`)
}
if (
workflowKind === 'optimization' &&
(!solve.result_provenance?.optimization_spec_hash ||
!solve.result_provenance?.result_payload_hash ||
!validate.result_provenance?.validation_report_hash)
) {
throw new Error(`${planItem.scenarioId}: optimization workflow provenance hashes missing`)
}
if (workflowKind === 'validation' && !validation) {
throw new Error(`${planItem.scenarioId}: observed validation summary missing`)
}
if (workflowKind === 'validation' && !validate.result_provenance?.validation_report_hash) {
throw new Error(`${planItem.scenarioId}: validation workflow report hash missing`)
}
if (workflowKind === 'recovery' && !ledgerSummary) {
throw new Error(`${planItem.scenarioId}: recovery EvidenceLedger summary missing`)
}
if (workflowKind === 'recovery' && !ledger.result_provenance?.result_payload_hash) {
throw new Error(`${planItem.scenarioId}: recovery EvidenceLedger result hash missing`)
}
const problemFamily =
solve?.result_provenance?.problem_family ??
validate?.result_provenance?.problem_family ??
@@ -320,6 +574,49 @@ const buildChapter = async (planItem, order) => {
pack.manifest.tags.find((tag) => ['lp', 'milp', 'qp', 'qcp', 'cp'].includes(tag)) ??
'validation'
const stability = stabilityMatrix.groups.find((group) => group.scenarioId === planItem.scenarioId) ?? null
const resultStatus =
workflowKind === 'optimization'
? solveSummary.status
: workflowKind === 'validation'
? validation.verdict
: 'recovered'
const validationVerdict = validation?.verdict ?? null
if (pack.oracle.expectedStatuses?.length && !pack.oracle.expectedStatuses.includes(resultStatus)) {
throw new Error(`${planItem.scenarioId}: observed result ${resultStatus} differs from Scenario Pack oracle`)
}
if (
source.data.diagnostics?.observedValidationVerdict != null &&
source.data.diagnostics.observedValidationVerdict !== validationVerdict
) {
throw new Error(`${planItem.scenarioId}: validation summary differs from case-run diagnostics`)
}
const sourceMessagesSha256 = sha256(readFileSync(messagesFile))
const evidence = {
runId: planItem.runId,
conversationId: planItem.conversationId,
sourceMessagesSha256,
sourceSessionSha256: sha256(readFileSync(sessionFile)),
sourceToolInvocationsSha256: sha256(readFileSync(invocationsFile)),
optimizationSpecSha256: solve?.result_provenance?.optimization_spec_hash ?? null,
compiledRequestSha256: solve?.result_provenance?.compiled_request_hash ?? null,
resultPayloadSha256:
solve?.result_provenance?.result_payload_hash ?? ledger?.result_provenance?.result_payload_hash ?? null,
validationReportSha256: validate?.result_provenance?.validation_report_hash ?? null,
}
const decisionTree = buildDecisionTree({
pack,
packSha256: sha256(readFileSync(packEntry.file)),
evidence,
workflowKind,
observed: {
workflowKind,
runStatus: source.data.status,
solveStatus: solveSummary?.status ?? null,
validationVerdict,
},
problemFamily,
engineId: solve?.result_provenance?.engine_id ?? null,
})
const media = []
for (const screenshot of planItem.screenshots) {
@@ -379,8 +676,8 @@ const buildChapter = async (planItem, order) => {
baseline: pack.businessContract.baseline,
outOfScope: pack.businessContract.outOfScope,
problemFamily,
resultStatus: pack.oracle.expectedStatuses?.[0] ?? source.data.status,
validationVerdict: source.data.diagnostics?.observedValidationVerdict ?? null,
resultStatus,
validationVerdict,
engineId: solve?.result_provenance?.engine_id ?? null,
engineRequirement: solve?.result_provenance
? {
@@ -390,17 +687,8 @@ const buildChapter = async (planItem, order) => {
capabilitySha256: solve.result_provenance.capability_hash,
}
: null,
evidence: {
runId: planItem.runId,
conversationId: planItem.conversationId,
sourceMessagesSha256: sha256(readFileSync(messagesFile)),
sourceSessionSha256: sha256(readFileSync(sessionFile)),
sourceToolInvocationsSha256: sha256(readFileSync(invocationsFile)),
optimizationSpecSha256: solve?.result_provenance?.optimization_spec_hash ?? null,
compiledRequestSha256: solve?.result_provenance?.compiled_request_hash ?? null,
resultPayloadSha256: solve?.result_provenance?.result_payload_hash ?? null,
validationReportSha256: validate?.result_provenance?.validation_report_hash ?? null,
},
evidence,
decisionTree,
counts: {
rawConversationMessages: conversationMessages.length,
visibleConversationMessages: renderedMessages.filter((item) => !item.empty).length,
@@ -461,6 +749,7 @@ cpSync(join(appRoot, 'node_modules', 'katex', 'dist', 'katex.min.css'), join(out
cpSync(join(appRoot, 'node_modules', 'katex', 'dist', 'fonts'), join(outputRoot, 'assets', 'fonts'), {
recursive: true,
})
cpSync(join(codeRoot, 'decision-tree.schema.json'), join(outputRoot, 'data', 'decision-tree.schema.json'))
const chapters = []
for (const [index, scenarioId] of scenarioOrder.entries()) {
@@ -502,7 +791,7 @@ const manifest = {
title: '通用求解器智能体团队 · 真实场景验证书',
generatedAt: new Date().toISOString(),
contentRootSha256,
reportVersion: '3.3.0',
reportVersion: '3.4.0',
platformCommit: latestPlatformRegression.platform.commit,
agentSkillVersion: latestPlatformRegression.platform.agentSkillVersion,
evidencePlatformCommit: historicalPlatformCommit,
@@ -513,6 +802,7 @@ const manifest = {
latestPlatformRegression,
counts: {
chapters: chapters.length,
decisionTrees: chapters.length,
screenshots: screenshotPlan.scenarios.reduce((total, item) => total + item.screenshots.length, 0),
representativeStabilityPasses,
},
@@ -552,6 +842,7 @@ writeFileSync(
integrityEntries: expectedIntegrityEntries,
gates: {
scenarioPacks: 'pass',
decisionTrees: 'pass',
stability: 'pass',
screenshots: 'pass',
publicReleasePrivacy: 'pass',

View File

@@ -1,5 +1,7 @@
import { existsSync, globSync, readFileSync, statSync, writeFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import {
assertPublicText,
canonical,
@@ -8,6 +10,13 @@ import {
sha256,
} from './public-release-policy.mjs'
const codeRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const require = createRequire(import.meta.url)
const Ajv = require('ajv').default
const ajv = new Ajv({ allErrors: true, strict: true })
const validateDecisionTreeSchema = ajv.compile(
JSON.parse(readFileSync(join(codeRoot, 'decision-tree.schema.json'), 'utf8'))
)
const inputRoot = resolve(process.env.SOLVER_REPORT_INPUT_ROOT ?? process.env.BUILDER_ROOT ?? process.cwd())
const dist = process.env.SOLVER_REPORT_OUTPUT_ROOT ?? join(inputRoot, 'report-web', 'dist')
const validationFile =
@@ -29,6 +38,12 @@ const caseRunById = new Map(
return [data.runId, { file, data, runDir: join(dirname(file), 'run') }]
})
)
const packById = new Map(
globSync(join(inputRoot, 'scenario-packs', '*', '2.0.0', 'scenario-pack.json')).map((file) => {
const pack = JSON.parse(readFileSync(file, 'utf8'))
return [pack.manifest.scenarioId, { file, pack }]
})
)
const screenshotPlan = JSON.parse(
readFileSync(
process.env.SOLVER_REPORT_PUBLIC_SCREENSHOT_PLAN_FILE ??
@@ -60,6 +75,8 @@ const { attestation: publicReleaseAttestation, sha256: publicReleaseAttestationS
loadPublicReleaseAttestation(publicAttestationFile, publicEvidenceRoots)
if (manifest.chapters.length !== 24) failures.push({ issue: 'chapter-count', actual: manifest.chapters.length })
if (manifest.counts.decisionTrees !== 24)
failures.push({ issue: 'decision-tree-count', actual: manifest.counts.decisionTrees })
if (manifest.counts.screenshots !== 174)
failures.push({ issue: 'screenshot-count', actual: manifest.counts.screenshots })
const historicalEvidenceContentRootSha256 = sha256(
@@ -105,6 +122,7 @@ if (
canonical(build.publicEvidenceRoots) !== canonical(manifest.publicEvidenceRoots) ||
canonical(build.counts) !== canonical(manifest.counts) ||
build.integrityEntries !== integrity.entries.length ||
build.gates?.decisionTrees !== 'pass' ||
build.gates?.publicReleasePrivacy !== 'pass' ||
!integrity.entries.some((entry) => entry.path === 'build.json')
)
@@ -117,6 +135,7 @@ try {
let formulas = 0
let screenshotCount = 0
let decisionTreeCount = 0
for (const index of manifest.chapters) {
const file = join(dist, index.json)
const script = join(dist, index.script)
@@ -155,6 +174,135 @@ for (const index of manifest.chapters) {
}
formulas += chapter.formulaStats.rendered
screenshotCount += chapter.media.length
const source = caseRunById.get(chapter.evidence.runId)
const sourceMessagesFile = source ? join(source.runDir, 'messages.jsonl') : null
const sourceSessionFile = source ? join(source.runDir, 'sessions', 'session.jsonl') : null
const sourceInvocationsFile = source ? join(source.runDir, 'receipts', 'tool-invocations.jsonl') : null
const sourceInvocations = source ? readJsonLines(sourceInvocationsFile) : []
const sourceSession = source ? readJsonLines(sourceSessionFile) : []
const finishedReceipts = sourceInvocations.filter((item) => item.phase === 'finished' && item.status === 'success')
const finishedToolNames = finishedReceipts.map((item) => item.tool_name)
const parsedSummaries = sourceSession
.filter((event) => event.type === 'tool_use_summary')
.map((event) => {
try {
return { event, summary: JSON.parse(event.data?.summary ?? '') }
} catch {
return null
}
})
.filter((entry) => entry?.summary && typeof entry.summary === 'object' && !Array.isArray(entry.summary))
const solveReceipt = finishedReceipts.find((item) => item.tool_name === 'OptimizationSolve')
const validationReceipt = [...finishedReceipts].reverse().find((item) => item.tool_name === 'OptimizationValidate')
const ledgerReceipt = [...finishedReceipts].reverse().find((item) => item.tool_name === 'OptimizationEvidenceLedger')
const observedSolveSummary = solveReceipt
? typeof solveReceipt.result_provenance?.result_payload_hash === 'string'
? parsedSummaries.find(
({ summary }) => summary.result_payload_hash === solveReceipt.result_provenance?.result_payload_hash
)?.summary ?? null
: null
: null
const observedValidationSummary = validationReceipt
? typeof validationReceipt.result_provenance?.validation_report_hash === 'string'
? [...parsedSummaries]
.reverse()
.find(
({ event, summary }) =>
['pass', 'fail'].includes(summary.verdict) &&
event.data?.metadata?.validation_report_hash === validationReceipt.result_provenance?.validation_report_hash
)?.summary ?? null
: null
: null
const observedLedgerSummary = ledgerReceipt
? [...parsedSummaries]
.reverse()
.find(({ summary }) => summary.schema_version === 'solver.optimization-evidence-ledger/v1')?.summary ?? null
: null
const tree = chapter.decisionTree
const treeNodes = new Map(tree?.nodes?.map((node) => [node.id, node]) ?? [])
const packEntry = packById.get(index.scenarioId)
if (
!validateDecisionTreeSchema(tree) ||
tree?.schemaVersion !== 'solver.human-agent-decision-tree/v1' ||
treeNodes.size !== tree?.nodes?.length ||
!treeNodes.has(tree?.rootNodeId) ||
tree?.edges?.some((edge) => !treeNodes.has(edge.from) || !treeNodes.has(edge.to)) ||
!packEntry ||
tree?.evidenceBindings?.scenarioPackSha256 !== sha256(readFileSync(packEntry.file)) ||
tree?.evidenceBindings?.runId !== chapter.evidence.runId ||
tree?.evidenceBindings?.sourceMessagesSha256 !== chapter.evidence.sourceMessagesSha256 ||
tree?.evidenceBindings?.sourceSessionSha256 !== chapter.evidence.sourceSessionSha256 ||
tree?.evidenceBindings?.sourceToolInvocationsSha256 !== chapter.evidence.sourceToolInvocationsSha256 ||
tree?.evidenceBindings?.optimizationSpecSha256 !== chapter.evidence.optimizationSpecSha256 ||
tree?.evidenceBindings?.resultPayloadSha256 !== chapter.evidence.resultPayloadSha256 ||
tree?.evidenceBindings?.validationReportSha256 !== chapter.evidence.validationReportSha256 ||
!source ||
tree?.evidenceBindings?.sourceMessagesSha256 !== sha256(readFileSync(sourceMessagesFile)) ||
tree?.evidenceBindings?.sourceSessionSha256 !== sha256(readFileSync(sourceSessionFile)) ||
tree?.evidenceBindings?.sourceToolInvocationsSha256 !== sha256(readFileSync(sourceInvocationsFile)) ||
source.data.status !== 'passed' ||
tree?.observed?.runStatus !== source.data.status ||
!['optimization', 'validation', 'recovery'].includes(tree?.observed?.workflowKind) ||
(tree?.observed?.workflowKind === 'optimization' &&
(!finishedToolNames.includes('OptimizationSolve') ||
!finishedToolNames.includes('OptimizationValidate') ||
!observedSolveSummary ||
!observedValidationSummary ||
tree.observed.solveStatus !== observedSolveSummary.status ||
tree.observed.validationVerdict !== observedValidationSummary.verdict ||
!solveReceipt?.result_provenance?.optimization_spec_hash ||
tree.evidenceBindings.optimizationSpecSha256 !== solveReceipt.result_provenance.optimization_spec_hash ||
tree.evidenceBindings.resultPayloadSha256 !== solveReceipt?.result_provenance?.result_payload_hash ||
!validationReceipt?.result_provenance?.validation_report_hash ||
tree.evidenceBindings.validationReportSha256 !== validationReceipt?.result_provenance?.validation_report_hash ||
!tree.evidenceBindings.resultPayloadSha256 ||
chapter.resultStatus !== tree.observed.solveStatus ||
chapter.validationVerdict !== tree.observed.validationVerdict)) ||
(tree?.observed?.workflowKind === 'validation' &&
(finishedToolNames.includes('OptimizationCompile') ||
finishedToolNames.includes('OptimizationSolve') ||
tree.observed.solveStatus !== null ||
!finishedToolNames.includes('OptimizationValidate') ||
!observedValidationSummary ||
tree.observed.validationVerdict !== observedValidationSummary.verdict ||
!validationReceipt?.result_provenance?.validation_report_hash ||
tree.evidenceBindings.validationReportSha256 !== validationReceipt?.result_provenance?.validation_report_hash ||
treeNodes.has('model') ||
chapter.resultStatus !== tree.observed.validationVerdict)) ||
(tree?.observed?.workflowKind === 'recovery' &&
(finishedToolNames.includes('OptimizationCompile') ||
finishedToolNames.includes('OptimizationSolve') ||
finishedToolNames.includes('OptimizationValidate') ||
tree.observed.solveStatus !== null ||
tree.observed.validationVerdict !== null ||
!finishedToolNames.includes('OptimizationEvidenceLedger') ||
!observedLedgerSummary ||
!ledgerReceipt?.result_provenance?.result_payload_hash ||
tree.evidenceBindings.resultPayloadSha256 !== ledgerReceipt?.result_provenance?.result_payload_hash ||
treeNodes.has('model') ||
!tree.evidenceBindings.resultPayloadSha256 ||
chapter.resultStatus !== 'recovered'))
) {
failures.push({ scenarioId: index.scenarioId, issue: 'decision-tree-contract-or-evidence-binding' })
} else {
const selectedEdges = tree.edges.filter((edge) => edge.selected)
const selectedByFrom = new Map()
for (const edge of selectedEdges) selectedByFrom.set(edge.from, [...(selectedByFrom.get(edge.from) ?? []), edge])
let cursor = tree.rootNodeId
const visited = new Set()
while (selectedByFrom.get(cursor)?.length === 1 && !visited.has(cursor)) {
visited.add(cursor)
cursor = selectedByFrom.get(cursor)[0].to
}
if (
cursor !== 'outcome' ||
visited.size !== selectedEdges.length ||
[...selectedByFrom.values()].some((outgoing) => outgoing.length !== 1) ||
tree.nodes.some((node) => node.id !== tree.rootNodeId && !tree.edges.some((edge) => edge.to === node.id))
)
failures.push({ scenarioId: index.scenarioId, issue: 'decision-tree-selected-path' })
else decisionTreeCount += 1
}
for (const image of chapter.media) {
const original = join(dist, image.original)
if (!existsSync(original) || sha256(readFileSync(original)) !== image.sha256) {
@@ -165,10 +313,9 @@ for (const index of manifest.chapters) {
failures.push({ scenarioId: index.scenarioId, issue: 'thumbnail-missing', path: thumb })
}
}
const source = caseRunById.get(chapter.evidence.runId)
if (!source) failures.push({ scenarioId: index.scenarioId, issue: 'source-run-missing' })
else {
const raw = readJsonLines(join(source.runDir, 'messages.jsonl')).filter((item) =>
const raw = readJsonLines(sourceMessagesFile).filter((item) =>
['user', 'assistant'].includes(item.role)
)
const projected = chapter.timeline.filter((item) => item.kind === 'message')
@@ -194,6 +341,7 @@ for (const index of manifest.chapters) {
}
if (screenshotCount !== 174) failures.push({ issue: 'chapter-screenshot-sum', actual: screenshotCount })
if (decisionTreeCount !== 24) failures.push({ issue: 'chapter-decision-tree-sum', actual: decisionTreeCount })
for (const entry of integrity.entries) {
const file = join(dist, entry.path)
if (!existsSync(file)) failures.push({ issue: 'integrity-file-missing', path: entry.path })
@@ -209,6 +357,7 @@ for (const required of [
'assets/report.js',
'assets/katex.min.css',
'data/manifest.js',
'data/decision-tree.schema.json',
'data/latest-platform-regression.json',
]) {
if (!existsSync(join(dist, required))) failures.push({ issue: 'required-asset', path: required })
@@ -220,6 +369,7 @@ const report = {
buildId: manifest.buildId,
chapters: manifest.chapters.length,
screenshots: screenshotCount,
decisionTrees: decisionTreeCount,
formulas,
integrityEntries: integrity.entries.length,
bytes: globSync(join(dist, '**', '*'))

View File

@@ -40,6 +40,8 @@ manifest = json.loads((root / "data" / "manifest.json").read_text(encoding="utf-
build = json.loads((root / "build.json").read_text(encoding="utf-8"))
if build.get("gates", {}).get("publicReleasePrivacy") != "pass":
failures.append({"path": "build.json", "issue": "public-release-privacy-gate"})
if build.get("gates", {}).get("decisionTrees") != "pass":
failures.append({"path": "build.json", "issue": "decision-tree-gate"})
if build.get("publicReleaseAttestationSha256") != manifest.get("publicReleaseAttestationSha256"):
failures.append({"path": "build.json", "issue": "public-release-attestation-mismatch"})
if (