mirror of
https://git.openapi.site/https://github.com/desirecore/agent-desirecore.git
synced 2026-09-05 20:43:43 +08:00
feat: 增加人机共管场景决策树 (#8)
Co-authored-by: yige <yige@yigedeMacBook-Neo.local>
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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, '**', '*'))
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user