mirror of
https://git.openapi.site/https://github.com/desirecore/agent-desirecore.git
synced 2026-09-05 19:43:47 +08:00
feat: 增加求解器验证书 Web (#7)
Co-authored-by: yige <yige@yigedeMacBook-Neo.local>
This commit is contained in:
99
web/solver-report/scripts/audit-screenshot-privacy.swift
Normal file
99
web/solver-report/scripts/audit-screenshot-privacy.swift
Normal file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env swift
|
||||
import AppKit
|
||||
import Foundation
|
||||
import Vision
|
||||
|
||||
enum AuditError: Error, CustomStringConvertible {
|
||||
case usage
|
||||
case malformedPlan(String)
|
||||
case unsafePath(String)
|
||||
case unreadableImage(String)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .usage:
|
||||
return "usage: audit-screenshot-privacy.swift <input-root> <screenshot-plan.json> <ocr-output.json>"
|
||||
case .malformedPlan(let detail):
|
||||
return "malformed screenshot plan: \(detail)"
|
||||
case .unsafePath(let path):
|
||||
return "screenshot path escapes input root: \(path)"
|
||||
case .unreadableImage(let path):
|
||||
return "cannot decode screenshot: \(path)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func requiredString(_ object: [String: Any], _ key: String) throws -> String {
|
||||
guard let value = object[key] as? String, !value.isEmpty else {
|
||||
throw AuditError.malformedPlan("missing \(key)")
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
do {
|
||||
guard CommandLine.arguments.count == 4 else { throw AuditError.usage }
|
||||
let inputRoot = URL(fileURLWithPath: CommandLine.arguments[1]).standardizedFileURL
|
||||
let planURL = URL(fileURLWithPath: CommandLine.arguments[2]).standardizedFileURL
|
||||
let outputURL = URL(fileURLWithPath: CommandLine.arguments[3]).standardizedFileURL
|
||||
let planData = try Data(contentsOf: planURL)
|
||||
guard
|
||||
let plan = try JSONSerialization.jsonObject(with: planData) as? [String: Any],
|
||||
let scenarios = plan["scenarios"] as? [[String: Any]]
|
||||
else {
|
||||
throw AuditError.malformedPlan("scenarios must be an array")
|
||||
}
|
||||
|
||||
var entries: [[String: Any]] = []
|
||||
for scenario in scenarios {
|
||||
let scenarioId = try requiredString(scenario, "scenarioId")
|
||||
guard let screenshots = scenario["screenshots"] as? [[String: Any]] else {
|
||||
throw AuditError.malformedPlan("\(scenarioId).screenshots must be an array")
|
||||
}
|
||||
for screenshot in screenshots {
|
||||
let relativePath = try requiredString(screenshot, "file")
|
||||
let imageURL = inputRoot.appendingPathComponent(relativePath).standardizedFileURL
|
||||
let rootPrefix = inputRoot.path.hasSuffix("/") ? inputRoot.path : inputRoot.path + "/"
|
||||
guard imageURL.path.hasPrefix(rootPrefix) else { throw AuditError.unsafePath(relativePath) }
|
||||
|
||||
let recognizedLines: [[String: Any]] = try autoreleasepool {
|
||||
guard
|
||||
let image = NSImage(contentsOf: imageURL),
|
||||
let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil)
|
||||
else {
|
||||
throw AuditError.unreadableImage(relativePath)
|
||||
}
|
||||
let request = VNRecognizeTextRequest()
|
||||
request.recognitionLevel = .accurate
|
||||
request.recognitionLanguages = ["zh-Hans", "en-US"]
|
||||
request.usesLanguageCorrection = true
|
||||
try VNImageRequestHandler(cgImage: cgImage, options: [:]).perform([request])
|
||||
return (request.results ?? []).compactMap { observation in
|
||||
guard let text = observation.topCandidates(1).first?.string else { return nil }
|
||||
let box = observation.boundingBox
|
||||
return [
|
||||
"text": text,
|
||||
"boundingBox": [box.origin.x, box.origin.y, box.size.width, box.size.height],
|
||||
]
|
||||
}
|
||||
}
|
||||
entries.append([
|
||||
"scenarioId": scenarioId,
|
||||
"file": relativePath,
|
||||
"lines": recognizedLines,
|
||||
])
|
||||
FileHandle.standardError.write(Data("OCR \(entries.count): \(relativePath)\n".utf8))
|
||||
}
|
||||
}
|
||||
|
||||
let output: [String: Any] = [
|
||||
"schemaVersion": "solver.screenshot-privacy-ocr/v1",
|
||||
"generatedAt": ISO8601DateFormatter().string(from: Date()),
|
||||
"engine": "Apple Vision VNRecognizeTextRequest accurate zh-Hans+en-US",
|
||||
"entries": entries,
|
||||
]
|
||||
let outputData = try JSONSerialization.data(withJSONObject: output, options: [.prettyPrinted, .sortedKeys])
|
||||
try outputData.write(to: outputURL, options: .atomic)
|
||||
} catch {
|
||||
FileHandle.standardError.write(Data("\(error)\n".utf8))
|
||||
exit(1)
|
||||
}
|
||||
591
web/solver-report/scripts/build-report-web.mjs
Normal file
591
web/solver-report/scripts/build-report-web.mjs
Normal file
@@ -0,0 +1,591 @@
|
||||
import { createRequire } from 'node:module'
|
||||
import { cpSync, existsSync, globSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
||||
import { basename, dirname, extname, join, relative, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import {
|
||||
assertPublicText,
|
||||
canonical,
|
||||
computePublicEvidenceRoots,
|
||||
loadPublicReleaseAttestation,
|
||||
projectPublicProvenance,
|
||||
resolveSafeOutputRoot,
|
||||
sha256,
|
||||
} from './public-release-policy.mjs'
|
||||
|
||||
const codeRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const inputRoot = resolve(process.env.SOLVER_REPORT_INPUT_ROOT ?? process.env.BUILDER_ROOT ?? process.cwd())
|
||||
const configuredAppRoot = process.env.DESIRECORE_APP_ROOT
|
||||
if (!configuredAppRoot) throw new Error('DESIRECORE_APP_ROOT is required')
|
||||
const appRoot = resolve(configuredAppRoot)
|
||||
const requireFromApp = createRequire(join(appRoot, 'package.json'))
|
||||
const { marked } = requireFromApp('marked')
|
||||
const katex = requireFromApp('katex')
|
||||
const sharp = requireFromApp('sharp')
|
||||
const Ajv = requireFromApp('ajv').default
|
||||
const addFormats = requireFromApp('ajv-formats').default
|
||||
const ajv = new Ajv({ allErrors: true, strict: true })
|
||||
addFormats(ajv)
|
||||
const validatePublicReleaseAttestationSchema = ajv.compile(
|
||||
JSON.parse(readFileSync(join(codeRoot, 'public-release-attestation.schema.json'), 'utf8'))
|
||||
)
|
||||
|
||||
const outputRoot = resolveSafeOutputRoot({
|
||||
inputRoot,
|
||||
codeRoot,
|
||||
appRoot,
|
||||
requestedOutputRoot: process.env.SOLVER_REPORT_OUTPUT_ROOT ?? join(inputRoot, 'report-web', 'dist'),
|
||||
})
|
||||
const sourceRoot = join(codeRoot, 'source')
|
||||
const sourceScreenshotPlan = JSON.parse(
|
||||
readFileSync(join(inputRoot, 'evidence', 'screenshots', 'screenshot-plan.json'), 'utf8')
|
||||
)
|
||||
const publicScreenshotPlanFile =
|
||||
process.env.SOLVER_REPORT_PUBLIC_SCREENSHOT_PLAN_FILE ??
|
||||
join(inputRoot, 'evidence', 'screenshots', 'public-screenshot-plan.json')
|
||||
const screenshotPlan = JSON.parse(readFileSync(publicScreenshotPlanFile, 'utf8'))
|
||||
const screenshotValidation = JSON.parse(
|
||||
readFileSync(join(inputRoot, 'evidence', 'screenshots', 'screenshot-validation.json'), 'utf8')
|
||||
)
|
||||
const screenshotPrivacyValidation = JSON.parse(
|
||||
readFileSync(
|
||||
process.env.SOLVER_REPORT_SCREENSHOT_PRIVACY_VALIDATION_FILE ??
|
||||
join(inputRoot, 'evidence', 'screenshots', 'public-screenshot-privacy-validation.json'),
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
const historicalBuildManifest = JSON.parse(readFileSync(join(inputRoot, 'build-manifest.json'), 'utf8'))
|
||||
const stabilityMatrix = JSON.parse(readFileSync(join(inputRoot, 'evidence', 'stability', 'stability-matrix.json'), 'utf8'))
|
||||
if (!screenshotValidation.passed || !stabilityMatrix.passed)
|
||||
throw new Error('M8 evidence gates must pass before Web build')
|
||||
if (
|
||||
screenshotPlan.schemaVersion !== 'solver.public-screenshot-plan/v1' ||
|
||||
screenshotPlan.sourcePlanSha256 !== sha256(canonical(sourceScreenshotPlan)) ||
|
||||
screenshotPlan.scenarios.length !== sourceScreenshotPlan.scenarios.length
|
||||
) {
|
||||
throw new Error('public screenshot plan is missing or differs from the source evidence plan')
|
||||
}
|
||||
for (const sourceScenario of sourceScreenshotPlan.scenarios) {
|
||||
const publicScenario = screenshotPlan.scenarios.find((candidate) => candidate.scenarioId === sourceScenario.scenarioId)
|
||||
if (
|
||||
!publicScenario ||
|
||||
publicScenario.runId !== sourceScenario.runId ||
|
||||
publicScenario.conversationId !== sourceScenario.conversationId ||
|
||||
publicScenario.screenshots.length !== sourceScenario.screenshots.length ||
|
||||
publicScenario.screenshots.some(
|
||||
(screenshot, index) => screenshot.sourceSha256 !== sourceScenario.screenshots[index].sha256
|
||||
)
|
||||
) {
|
||||
throw new Error(`${sourceScenario.scenarioId}: public screenshot derivation differs from source evidence`)
|
||||
}
|
||||
}
|
||||
const latestRegressionFile =
|
||||
process.env.LATEST_PLATFORM_REGRESSION_FILE ??
|
||||
join(inputRoot, 'evidence', 'latest-platform-regression-20260830', 'summary.json')
|
||||
const latestPlatformRegression = JSON.parse(readFileSync(latestRegressionFile, 'utf8'))
|
||||
if (
|
||||
latestPlatformRegression.schemaVersion !== 'solver.latest-platform-regression/v1' ||
|
||||
latestPlatformRegression.passed !== true
|
||||
) {
|
||||
throw new Error('latest platform regression summary is missing, unsupported, or not passed')
|
||||
}
|
||||
const historicalPlatformCommit = historicalBuildManifest.platform?.commit
|
||||
const historicalAgentSkillVersion = historicalBuildManifest.skill?.version
|
||||
const representativeStabilityPasses = stabilityMatrix.groups?.reduce(
|
||||
(total, group) => total + 1 + (group.freshAndPerturbedRuns?.length ?? 0),
|
||||
0
|
||||
)
|
||||
if (
|
||||
historicalBuildManifest.schema_version !== 'solver-agent-team-build/v1' ||
|
||||
typeof historicalPlatformCommit !== 'string' ||
|
||||
typeof historicalAgentSkillVersion !== 'string' ||
|
||||
!Number.isInteger(representativeStabilityPasses) ||
|
||||
representativeStabilityPasses < 1
|
||||
) {
|
||||
throw new Error('historical build manifest cannot derive public evidence ownership')
|
||||
}
|
||||
if (
|
||||
!historicalPlatformCommit.startsWith(latestPlatformRegression.historicalEvidence?.platformCommit ?? '') ||
|
||||
latestPlatformRegression.historicalEvidence?.representativeStabilityPasses !== representativeStabilityPasses
|
||||
) {
|
||||
throw new Error('latest regression historical attribution differs from the source evidence')
|
||||
}
|
||||
|
||||
const scenarioOrder = [
|
||||
'rw.territory-assignment',
|
||||
'rw.resource-allocation',
|
||||
'rw.performance-target',
|
||||
'rw.customer-task-scheduling',
|
||||
'rw.workforce-shift',
|
||||
'rw.insufficient-capacity-iis',
|
||||
'of.api-baseline',
|
||||
'of.diet',
|
||||
'of.facility-location',
|
||||
'of.weekly-workforce',
|
||||
'of.soap-production',
|
||||
'of.ad-traffic',
|
||||
'of.min-cost-flow',
|
||||
'of.max-flow',
|
||||
'of.portfolio',
|
||||
'of.iis-diagnosis',
|
||||
'of.seven-day-shift',
|
||||
'boundary.qp-unsupported',
|
||||
'qcp.risk-budget',
|
||||
'cp.single-machine-detection',
|
||||
'exploratory.two-person-shift',
|
||||
'validation.independent-pass',
|
||||
'validation.independent-fail',
|
||||
'recovery.cold-start',
|
||||
]
|
||||
|
||||
const sectionIds = [
|
||||
'scenario-and-decision',
|
||||
'input-and-provenance',
|
||||
'data-quality',
|
||||
'model-definition',
|
||||
'train-validation-test',
|
||||
'solve-process',
|
||||
'solve-result',
|
||||
'independent-validation',
|
||||
'assumptions-risks-boundary',
|
||||
'reproduction-and-conclusion',
|
||||
]
|
||||
|
||||
const readJsonLines = (file) =>
|
||||
readFileSync(file, 'utf8')
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line))
|
||||
const escapeHtml = (value) =>
|
||||
String(value ?? '')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''')
|
||||
|
||||
let activeFormulaStats = null
|
||||
const renderer = new marked.Renderer()
|
||||
renderer.html = ({ text }) => escapeHtml(text)
|
||||
marked.use({
|
||||
extensions: [
|
||||
{
|
||||
name: 'blockMath',
|
||||
level: 'block',
|
||||
start: (source) => source.indexOf('\\['),
|
||||
tokenizer(source) {
|
||||
const match = /^\\\[([\s\S]*?)\\\](?:\n|$)/.exec(source)
|
||||
return match ? { type: 'blockMath', raw: match[0], text: match[1].trim() } : undefined
|
||||
},
|
||||
renderer(token) {
|
||||
if (activeFormulaStats) {
|
||||
activeFormulaStats.source += 1
|
||||
activeFormulaStats.display += 1
|
||||
}
|
||||
const html = katex.renderToString(token.text, {
|
||||
throwOnError: true,
|
||||
strict: 'ignore',
|
||||
trust: false,
|
||||
output: 'htmlAndMathml',
|
||||
displayMode: true,
|
||||
})
|
||||
if (activeFormulaStats) activeFormulaStats.rendered += 1
|
||||
return `${html}\n`
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'inlineMath',
|
||||
level: 'inline',
|
||||
start: (source) => source.indexOf('\\('),
|
||||
tokenizer(source) {
|
||||
const match = /^\\\(([^\n]+?)\\\)/.exec(source)
|
||||
return match ? { type: 'inlineMath', raw: match[0], text: match[1].trim() } : undefined
|
||||
},
|
||||
renderer(token) {
|
||||
if (activeFormulaStats) activeFormulaStats.source += 1
|
||||
const html = katex.renderToString(token.text, {
|
||||
throwOnError: true,
|
||||
strict: 'ignore',
|
||||
trust: false,
|
||||
output: 'htmlAndMathml',
|
||||
displayMode: false,
|
||||
})
|
||||
if (activeFormulaStats) activeFormulaStats.rendered += 1
|
||||
return html
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const renderMarkdown = (markdown) => {
|
||||
const html = marked.parse(markdown, { gfm: true, breaks: true, renderer })
|
||||
if (typeof html !== 'string') throw new Error('marked returned non-string output')
|
||||
return html
|
||||
}
|
||||
|
||||
const splitFinalReport = (markdown, scenarioId) => {
|
||||
const matches = [...markdown.matchAll(/^##\s+(10|[1-9])\.\s+(.+)\s*$/gm)]
|
||||
if (matches.length !== 10 || matches.some((match, index) => Number(match[1]) !== index + 1)) {
|
||||
throw new Error(`${scenarioId}: final report is not the required ten-part structure`)
|
||||
}
|
||||
return matches.map((match, index) => {
|
||||
const start = match.index + match[0].length
|
||||
const end = matches[index + 1]?.index ?? markdown.length
|
||||
const source = markdown.slice(start, end).trim()
|
||||
return {
|
||||
id: sectionIds[index],
|
||||
number: index + 1,
|
||||
title: match[2].trim(),
|
||||
markdown: source,
|
||||
markdownSha256: sha256(source),
|
||||
html: renderMarkdown(source),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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 caseRunById = new Map(
|
||||
globSync(join(inputRoot, 'evidence', '**', 'case-run.json')).map((file) => {
|
||||
const data = JSON.parse(readFileSync(file, 'utf8'))
|
||||
return [data.runId, { file, data, runDir: join(dirname(file), 'run') }]
|
||||
})
|
||||
)
|
||||
|
||||
const parseSummaryJson = (event) => {
|
||||
try {
|
||||
return JSON.parse(event.data?.summary ?? '')
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const buildChapter = async (planItem, order) => {
|
||||
const source = caseRunById.get(planItem.runId)
|
||||
if (!source) throw new Error(`${planItem.scenarioId}: accepted Run evidence missing: ${planItem.runId}`)
|
||||
const packEntry = packById.get(planItem.scenarioId)
|
||||
if (!packEntry) throw new Error(`${planItem.scenarioId}: Scenario Pack missing`)
|
||||
const { pack } = packEntry
|
||||
const messagesFile = join(source.runDir, 'messages.jsonl')
|
||||
const sessionFile = join(source.runDir, 'sessions', 'session.jsonl')
|
||||
const invocationsFile = join(source.runDir, 'receipts', 'tool-invocations.jsonl')
|
||||
const messages = readJsonLines(messagesFile)
|
||||
const session = readJsonLines(sessionFile)
|
||||
const invocations = readJsonLines(invocationsFile)
|
||||
const conversationMessages = messages.filter((item) => ['user', 'assistant'].includes(item.role))
|
||||
const finalAssistant = conversationMessages.filter((item) => item.role === 'assistant' && item.content?.trim()).at(-1)
|
||||
if (!finalAssistant) throw new Error(`${planItem.scenarioId}: final assistant message missing`)
|
||||
|
||||
activeFormulaStats = { source: 0, rendered: 0, display: 0 }
|
||||
const renderedMessages = conversationMessages.map((message, index) => ({
|
||||
kind: 'message',
|
||||
sequence: index + 1,
|
||||
role: message.role,
|
||||
timestamp: message.timestamp,
|
||||
markdown: message.content ?? '',
|
||||
markdownSha256: sha256(message.content ?? ''),
|
||||
html: message.content?.trim() ? renderMarkdown(message.content) : '',
|
||||
empty: !message.content?.trim(),
|
||||
}))
|
||||
const toolEvents = invocations
|
||||
.filter((item) => item.phase === 'finished')
|
||||
.map((item, index) => ({
|
||||
kind: 'tool',
|
||||
sequence: index + 1,
|
||||
timestamp: Date.parse(item.timestamp),
|
||||
tool: item.tool_name,
|
||||
status: item.status,
|
||||
durationMs: item.duration_ms ?? null,
|
||||
provenance: projectPublicProvenance(item.result_provenance),
|
||||
}))
|
||||
const sections = splitFinalReport(finalAssistant.content, planItem.scenarioId)
|
||||
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 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 problemFamily =
|
||||
solve?.result_provenance?.problem_family ??
|
||||
validate?.result_provenance?.problem_family ??
|
||||
pack.oracle?.expectedProblemFamily ??
|
||||
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 media = []
|
||||
for (const screenshot of planItem.screenshots) {
|
||||
const sourceFile = join(inputRoot, screenshot.file)
|
||||
const name = basename(sourceFile)
|
||||
const originalRel = `media/original/${planItem.scenarioId}/${name}`
|
||||
const thumb756Rel = `media/thumb-756/${planItem.scenarioId}/${name.replace(/\.png$/, '.webp')}`
|
||||
const thumb1512Rel = `media/thumb-1512/${planItem.scenarioId}/${name.replace(/\.png$/, '.webp')}`
|
||||
const originalTarget = join(outputRoot, originalRel)
|
||||
const thumb756Target = join(outputRoot, thumb756Rel)
|
||||
const thumb1512Target = join(outputRoot, thumb1512Rel)
|
||||
mkdirSync(dirname(originalTarget), { recursive: true })
|
||||
mkdirSync(dirname(thumb756Target), { recursive: true })
|
||||
mkdirSync(dirname(thumb1512Target), { recursive: true })
|
||||
cpSync(sourceFile, originalTarget)
|
||||
await sharp(sourceFile)
|
||||
.resize({ width: 756, withoutEnlargement: true })
|
||||
.webp({ lossless: true, effort: 4 })
|
||||
.toFile(thumb756Target)
|
||||
await sharp(sourceFile)
|
||||
.resize({ width: 1512, withoutEnlargement: true })
|
||||
.webp({ lossless: true, effort: 4 })
|
||||
.toFile(thumb1512Target)
|
||||
media.push({
|
||||
stage:
|
||||
media.length === 0
|
||||
? 'conversation-start'
|
||||
: media.length === planItem.screenshots.length - 1
|
||||
? 'delivery-conclusion'
|
||||
: 'conversation-process',
|
||||
original: originalRel,
|
||||
thumb756: thumb756Rel,
|
||||
thumb1512: thumb1512Rel,
|
||||
width: screenshot.width,
|
||||
height: screenshot.height,
|
||||
sha256: screenshot.sha256,
|
||||
alt: `${pack.manifest.title}真实多轮会话第 ${media.length + 1} 段,展示${media.length === 0 ? '用户请求与智能体补问' : media.length === planItem.screenshots.length - 1 ? '验证结论与复现证据' : '工具过程或报告正文'}`,
|
||||
})
|
||||
}
|
||||
|
||||
const formulaStats = { ...activeFormulaStats }
|
||||
activeFormulaStats = null
|
||||
const chapter = {
|
||||
schemaVersion: 'solver.web-report-chapter/v1',
|
||||
scenarioId: planItem.scenarioId,
|
||||
scenarioVersion: pack.manifest.version,
|
||||
scenarioOrder: order,
|
||||
title: pack.manifest.title,
|
||||
scenarioFamily: pack.manifest.scenarioFamily,
|
||||
claimBoundary: pack.manifest.claimBoundary,
|
||||
tags: pack.manifest.tags,
|
||||
story: pack.businessContract.plainLanguageStory,
|
||||
decision: pack.businessContract.decision,
|
||||
professionalSummary: pack.businessContract.professionalSummary,
|
||||
objectives: pack.businessContract.objectives,
|
||||
constraints: pack.businessContract.constraints,
|
||||
baseline: pack.businessContract.baseline,
|
||||
outOfScope: pack.businessContract.outOfScope,
|
||||
problemFamily,
|
||||
resultStatus: pack.oracle.expectedStatuses?.[0] ?? source.data.status,
|
||||
validationVerdict: source.data.diagnostics?.observedValidationVerdict ?? null,
|
||||
engineId: solve?.result_provenance?.engine_id ?? null,
|
||||
engineRequirement: solve?.result_provenance
|
||||
? {
|
||||
problemFamily: solve.result_provenance.problem_family,
|
||||
irFamily: solve.result_provenance.ir_family,
|
||||
irVersion: solve.result_provenance.ir_version,
|
||||
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,
|
||||
},
|
||||
counts: {
|
||||
rawConversationMessages: conversationMessages.length,
|
||||
visibleConversationMessages: renderedMessages.filter((item) => !item.empty).length,
|
||||
toolEvents: toolEvents.length,
|
||||
screenshots: media.length,
|
||||
},
|
||||
formulaStats,
|
||||
timeline,
|
||||
sections,
|
||||
validation,
|
||||
stability: stability
|
||||
? {
|
||||
policy: '3 fresh + 1 natural-language perturbation',
|
||||
effectivePasses: 4,
|
||||
semanticConvergence: stability.assertions.semanticConvergence,
|
||||
singleSolvePerRun: stability.assertions.singleSolvePerRun,
|
||||
semanticSha256: stability.freshAndPerturbedRuns[0].optimizationSemanticHash,
|
||||
engineRequirementSha256: stability.freshAndPerturbedRuns[0].engineRequirementHash,
|
||||
}
|
||||
: { policy: 'standard real Run accepted; representative families use 3+1 gate', effectivePasses: 1 },
|
||||
media,
|
||||
}
|
||||
assertPublicText(chapter, `${planItem.scenarioId} chapter`)
|
||||
chapter.contentSha256 = sha256(canonical(chapter))
|
||||
return chapter
|
||||
}
|
||||
|
||||
const publicEvidenceRoots = computePublicEvidenceRoots({
|
||||
inputRoot,
|
||||
screenshotPlan,
|
||||
screenshotValidation,
|
||||
screenshotPrivacyValidation,
|
||||
caseRunById,
|
||||
})
|
||||
const publicAttestationFile =
|
||||
process.env.SOLVER_REPORT_PUBLIC_RELEASE_ATTESTATION_FILE ??
|
||||
join(inputRoot, 'evidence', 'public-release-attestation.json')
|
||||
const { attestation: publicReleaseAttestation, sha256: publicReleaseAttestationSha256 } =
|
||||
loadPublicReleaseAttestation(publicAttestationFile, publicEvidenceRoots)
|
||||
if (!validatePublicReleaseAttestationSchema(publicReleaseAttestation)) {
|
||||
throw new Error(`public release attestation schema validation failed: ${ajv.errorsText(validatePublicReleaseAttestationSchema.errors)}`)
|
||||
}
|
||||
if (
|
||||
!historicalPlatformCommit.startsWith(publicReleaseAttestation.metadata.platformCommit) ||
|
||||
publicReleaseAttestation.metadata.agentSkillVersion !== historicalAgentSkillVersion ||
|
||||
publicReleaseAttestation.metadata.representativeStabilityPasses !== representativeStabilityPasses
|
||||
) {
|
||||
throw new Error('public release attestation metadata differs from derived source evidence')
|
||||
}
|
||||
assertPublicText(latestPlatformRegression, 'latest platform regression')
|
||||
|
||||
rmSync(outputRoot, { recursive: true, force: true })
|
||||
mkdirSync(join(outputRoot, 'assets'), { recursive: true })
|
||||
mkdirSync(join(outputRoot, 'data', 'chapters'), { recursive: true })
|
||||
for (const name of ['index.html', 'report.css', 'report.js'])
|
||||
cpSync(join(sourceRoot, name), join(outputRoot, name === 'index.html' ? name : `assets/${name}`))
|
||||
cpSync(join(appRoot, 'node_modules', 'katex', 'dist', 'katex.min.css'), join(outputRoot, 'assets', 'katex.min.css'))
|
||||
cpSync(join(appRoot, 'node_modules', 'katex', 'dist', 'fonts'), join(outputRoot, 'assets', 'fonts'), {
|
||||
recursive: true,
|
||||
})
|
||||
|
||||
const chapters = []
|
||||
for (const [index, scenarioId] of scenarioOrder.entries()) {
|
||||
const item = screenshotPlan.scenarios.find((candidate) => candidate.scenarioId === scenarioId)
|
||||
if (!item) throw new Error(`screenshot plan missing ${scenarioId}`)
|
||||
const chapter = await buildChapter(item, index + 1)
|
||||
const fileName = `${String(index + 1).padStart(2, '0')}-${scenarioId}.json`
|
||||
const jsName = fileName.replace(/\.json$/, '.js')
|
||||
writeFileSync(join(outputRoot, 'data', 'chapters', fileName), `${JSON.stringify(chapter)}\n`)
|
||||
writeFileSync(
|
||||
join(outputRoot, 'data', 'chapters', jsName),
|
||||
`window.__REPORT_CHAPTERS__=window.__REPORT_CHAPTERS__||{};window.__REPORT_CHAPTERS__[${JSON.stringify(scenarioId)}]=${JSON.stringify(chapter)};\n`
|
||||
)
|
||||
chapters.push({
|
||||
scenarioId,
|
||||
order: index + 1,
|
||||
title: chapter.title,
|
||||
story: chapter.story,
|
||||
problemFamily: chapter.problemFamily,
|
||||
resultStatus: chapter.resultStatus,
|
||||
validationVerdict: chapter.validationVerdict,
|
||||
engineId: chapter.engineId,
|
||||
json: `data/chapters/${fileName}`,
|
||||
script: `data/chapters/${jsName}`,
|
||||
sha256: chapter.contentSha256,
|
||||
bytes: statSync(join(outputRoot, 'data', 'chapters', fileName)).size,
|
||||
screenshots: chapter.media.length,
|
||||
})
|
||||
}
|
||||
|
||||
const historicalEvidenceContentRootSha256 = sha256(
|
||||
canonical(chapters.map((item) => ({ scenarioId: item.scenarioId, sha256: item.sha256 })))
|
||||
)
|
||||
const latestPlatformRegressionSha256 = sha256(canonical(latestPlatformRegression))
|
||||
const contentRootSha256 = sha256(canonical({ historicalEvidenceContentRootSha256, latestPlatformRegressionSha256 }))
|
||||
const manifest = {
|
||||
schemaVersion: 'solver.web-report-manifest/v1',
|
||||
buildId: `${new Date().toISOString().replace(/[-:.]/g, '').slice(0, 15)}Z-${contentRootSha256.slice(0, 12)}`,
|
||||
title: '通用求解器智能体团队 · 真实场景验证书',
|
||||
generatedAt: new Date().toISOString(),
|
||||
contentRootSha256,
|
||||
reportVersion: '3.3.0',
|
||||
platformCommit: latestPlatformRegression.platform.commit,
|
||||
agentSkillVersion: latestPlatformRegression.platform.agentSkillVersion,
|
||||
evidencePlatformCommit: historicalPlatformCommit,
|
||||
publicReleaseAttestationSha256,
|
||||
publicEvidenceRoots,
|
||||
historicalEvidenceContentRootSha256,
|
||||
latestPlatformRegressionSha256,
|
||||
latestPlatformRegression,
|
||||
counts: {
|
||||
chapters: chapters.length,
|
||||
screenshots: screenshotPlan.scenarios.reduce((total, item) => total + item.screenshots.length, 0),
|
||||
representativeStabilityPasses,
|
||||
},
|
||||
chapters,
|
||||
rendering: {
|
||||
markdown: { engine: 'marked', version: requireFromApp('marked/package.json').version },
|
||||
math: { engine: 'katex', version: requireFromApp('katex/package.json').version, output: 'htmlAndMathml' },
|
||||
},
|
||||
}
|
||||
writeFileSync(join(outputRoot, 'data', 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`)
|
||||
writeFileSync(join(outputRoot, 'data', 'manifest.js'), `window.__REPORT_MANIFEST__=${JSON.stringify(manifest)};\n`)
|
||||
writeFileSync(
|
||||
join(outputRoot, 'data', 'latest-platform-regression.json'),
|
||||
`${JSON.stringify(latestPlatformRegression, null, 2)}\n`
|
||||
)
|
||||
|
||||
const mime = (file) =>
|
||||
({
|
||||
'.html': 'text/html',
|
||||
'.css': 'text/css',
|
||||
'.js': 'text/javascript',
|
||||
'.json': 'application/json',
|
||||
'.png': 'image/png',
|
||||
'.webp': 'image/webp',
|
||||
'.woff2': 'font/woff2',
|
||||
})[extname(file)] ?? 'application/octet-stream'
|
||||
const integrityEntriesBeforeBuild = globSync(join(outputRoot, '**', '*')).filter((file) => statSync(file).isFile()).length
|
||||
const expectedIntegrityEntries = integrityEntriesBeforeBuild + 1
|
||||
writeFileSync(
|
||||
join(outputRoot, 'build.json'),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
schemaVersion: 'solver.web-report-build/v1',
|
||||
buildId: manifest.buildId,
|
||||
contentRootSha256,
|
||||
counts: manifest.counts,
|
||||
integrityEntries: expectedIntegrityEntries,
|
||||
gates: {
|
||||
scenarioPacks: 'pass',
|
||||
stability: 'pass',
|
||||
screenshots: 'pass',
|
||||
publicReleasePrivacy: 'pass',
|
||||
markdown: 'pass',
|
||||
math: 'pass',
|
||||
},
|
||||
publicReleaseAttestationSha256,
|
||||
publicEvidenceRoots,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}\n`
|
||||
)
|
||||
const integrityEntries = globSync(join(outputRoot, '**', '*'))
|
||||
.filter((file) => statSync(file).isFile())
|
||||
.map((file) => {
|
||||
const bytes = readFileSync(file)
|
||||
return { path: relative(outputRoot, file), mime: mime(file), bytes: bytes.length, sha256: sha256(bytes) }
|
||||
})
|
||||
.sort((left, right) => left.path.localeCompare(right.path))
|
||||
const integrity = { schemaVersion: 'solver.web-report-integrity/v1', contentRootSha256, entries: integrityEntries }
|
||||
writeFileSync(join(outputRoot, 'integrity.json'), `${JSON.stringify(integrity, null, 2)}\n`)
|
||||
if (integrityEntries.length !== expectedIntegrityEntries) throw new Error('integrity entry count changed unexpectedly')
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
outputRoot,
|
||||
buildId: manifest.buildId,
|
||||
contentRootSha256,
|
||||
counts: manifest.counts,
|
||||
integrityEntries: integrityEntries.length,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
75
web/solver-report/scripts/create-privacy-contact-sheets.mjs
Normal file
75
web/solver-report/scripts/create-privacy-contact-sheets.mjs
Normal file
@@ -0,0 +1,75 @@
|
||||
import { createRequire } from 'node:module'
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
|
||||
const inputRoot = resolve(process.env.SOLVER_REPORT_INPUT_ROOT ?? process.cwd())
|
||||
const appRoot = process.env.DESIRECORE_APP_ROOT
|
||||
if (!appRoot) throw new Error('DESIRECORE_APP_ROOT is required')
|
||||
const reviewRoot = process.env.SOLVER_REPORT_PRIVACY_REVIEW_ROOT
|
||||
if (!reviewRoot) throw new Error('SOLVER_REPORT_PRIVACY_REVIEW_ROOT is required')
|
||||
const outputRoot = resolve(reviewRoot)
|
||||
const requireFromApp = createRequire(join(resolve(appRoot), 'package.json'))
|
||||
const sharp = requireFromApp('sharp')
|
||||
const plan = JSON.parse(readFileSync(join(inputRoot, 'evidence', 'screenshots', 'screenshot-plan.json'), 'utf8'))
|
||||
|
||||
const entries = plan.scenarios.flatMap((scenario) =>
|
||||
scenario.screenshots.map((screenshot, index) => ({
|
||||
scenarioId: scenario.scenarioId,
|
||||
index: index + 1,
|
||||
file: screenshot.file,
|
||||
sha256: screenshot.sha256,
|
||||
}))
|
||||
)
|
||||
const columns = 5
|
||||
const rows = 6
|
||||
const tileWidth = 240
|
||||
const tileHeight = 175
|
||||
const imageHeight = 145
|
||||
const perSheet = columns * rows
|
||||
const escapeXml = (value) =>
|
||||
String(value).replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char])
|
||||
|
||||
mkdirSync(outputRoot, { recursive: true })
|
||||
const sheets = []
|
||||
for (let offset = 0; offset < entries.length; offset += perSheet) {
|
||||
const page = entries.slice(offset, offset + perSheet)
|
||||
const tiles = []
|
||||
for (const entry of page) {
|
||||
const preview = await sharp(join(inputRoot, entry.file))
|
||||
.resize({ width: tileWidth, height: imageHeight, fit: 'contain', background: '#0b1120' })
|
||||
.png()
|
||||
.toBuffer()
|
||||
const label = `${entry.scenarioId} · ${entry.index} · ${entry.sha256.slice(0, 10)}`
|
||||
const tile = await sharp({
|
||||
create: { width: tileWidth, height: tileHeight, channels: 4, background: '#111a2d' },
|
||||
})
|
||||
.composite([
|
||||
{ input: preview, left: 0, top: 0 },
|
||||
{
|
||||
input: Buffer.from(
|
||||
`<svg width="${tileWidth}" height="30"><rect width="100%" height="100%" fill="#18243b"/><text x="8" y="19" fill="#dce7f4" font-family="Arial, sans-serif" font-size="10">${escapeXml(label)}</text></svg>`
|
||||
),
|
||||
left: 0,
|
||||
top: imageHeight,
|
||||
},
|
||||
])
|
||||
.png()
|
||||
.toBuffer()
|
||||
const position = tiles.length
|
||||
tiles.push({ input: tile, left: (position % columns) * tileWidth, top: Math.floor(position / columns) * tileHeight })
|
||||
}
|
||||
const file = `contact-sheet-${String(sheets.length + 1).padStart(2, '0')}.png`
|
||||
await sharp({
|
||||
create: { width: columns * tileWidth, height: rows * tileHeight, channels: 4, background: '#080d18' },
|
||||
})
|
||||
.composite(tiles)
|
||||
.png()
|
||||
.toFile(join(outputRoot, file))
|
||||
sheets.push({ file, entries: page })
|
||||
}
|
||||
|
||||
writeFileSync(
|
||||
join(outputRoot, 'review-index.json'),
|
||||
`${JSON.stringify({ schemaVersion: 'solver.privacy-contact-sheets/v1', screenshots: entries.length, sheets }, null, 2)}\n`
|
||||
)
|
||||
console.log(JSON.stringify({ outputRoot, screenshots: entries.length, sheets: sheets.length }, null, 2))
|
||||
143
web/solver-report/scripts/create-public-screenshots.mjs
Normal file
143
web/solver-report/scripts/create-public-screenshots.mjs
Normal file
@@ -0,0 +1,143 @@
|
||||
import { createRequire } from 'node:module'
|
||||
import { cpSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
||||
import { basename, dirname, join, relative, resolve } from 'node:path'
|
||||
import { canonical, publicTextViolations, sha256 } from './public-release-policy.mjs'
|
||||
|
||||
const inputRoot = resolve(process.env.SOLVER_REPORT_INPUT_ROOT ?? process.cwd())
|
||||
const appRoot = process.env.DESIRECORE_APP_ROOT
|
||||
const ocrFile = process.env.SOLVER_REPORT_SCREENSHOT_OCR_FILE
|
||||
if (!appRoot) throw new Error('DESIRECORE_APP_ROOT is required')
|
||||
if (!ocrFile) throw new Error('SOLVER_REPORT_SCREENSHOT_OCR_FILE is required')
|
||||
const requireFromApp = createRequire(join(resolve(appRoot), 'package.json'))
|
||||
const sharp = requireFromApp('sharp')
|
||||
const planFile = join(inputRoot, 'evidence', 'screenshots', 'screenshot-plan.json')
|
||||
const outputPlanFile = join(inputRoot, 'evidence', 'screenshots', 'public-screenshot-plan.json')
|
||||
const outputRoot = join(inputRoot, 'evidence', 'screenshots', 'public-redacted')
|
||||
const sourcePlan = JSON.parse(readFileSync(planFile, 'utf8'))
|
||||
const ocr = JSON.parse(readFileSync(ocrFile, 'utf8'))
|
||||
const sourceScreenshots = sourcePlan.scenarios.flatMap((scenario) =>
|
||||
scenario.screenshots.map((screenshot) => ({ scenarioId: scenario.scenarioId, ...screenshot }))
|
||||
)
|
||||
if (ocr.schemaVersion !== 'solver.screenshot-privacy-ocr/v1' || ocr.entries.length !== sourceScreenshots.length) {
|
||||
throw new Error('OCR result does not cover the source screenshot plan')
|
||||
}
|
||||
|
||||
const clamp = (value, minimum, maximum) => Math.max(minimum, Math.min(maximum, value))
|
||||
const overlaps = (left, right) =>
|
||||
left.left <= right.left + right.width &&
|
||||
right.left <= left.left + left.width &&
|
||||
left.top <= right.top + right.height &&
|
||||
right.top <= left.top + left.height
|
||||
const mergeRegions = (regions) => {
|
||||
const merged = []
|
||||
for (const region of regions) {
|
||||
const existing = merged.find((candidate) => overlaps(candidate, region))
|
||||
if (!existing) {
|
||||
merged.push({ ...region })
|
||||
continue
|
||||
}
|
||||
const right = Math.max(existing.left + existing.width, region.left + region.width)
|
||||
const bottom = Math.max(existing.top + existing.height, region.top + region.height)
|
||||
existing.left = Math.min(existing.left, region.left)
|
||||
existing.top = Math.min(existing.top, region.top)
|
||||
existing.width = right - existing.left
|
||||
existing.height = bottom - existing.top
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
let redactedScreenshots = 0
|
||||
let redactionRegions = 0
|
||||
const publicByFile = new Map()
|
||||
for (const [index, source] of sourceScreenshots.entries()) {
|
||||
const ocrEntry = ocr.entries[index]
|
||||
const sourceFile = join(inputRoot, source.file)
|
||||
if (
|
||||
ocrEntry?.scenarioId !== source.scenarioId ||
|
||||
ocrEntry?.file !== source.file ||
|
||||
sha256(readFileSync(sourceFile)) !== source.sha256
|
||||
) {
|
||||
throw new Error(`${source.file}: OCR source identity differs from screenshot plan`)
|
||||
}
|
||||
const metadata = await sharp(sourceFile).metadata()
|
||||
if (metadata.width !== source.width || metadata.height !== source.height) {
|
||||
throw new Error(`${source.file}: source dimensions differ from screenshot plan`)
|
||||
}
|
||||
const proposedRegions = ocrEntry.lines
|
||||
.filter((line) => publicTextViolations(line.text).length > 0)
|
||||
.map((line) => {
|
||||
const [x, y, width, height] = line.boundingBox
|
||||
const left = clamp(Math.floor(x * source.width) - 12, 0, source.width - 1)
|
||||
const top = clamp(Math.floor((1 - y - height) * source.height) - 8, 0, source.height - 1)
|
||||
const right = clamp(Math.ceil((x + width) * source.width) + 12, left + 1, source.width)
|
||||
const bottom = clamp(Math.ceil((1 - y) * source.height) + Math.ceil(height * source.height * 1.2) + 8, top + 1, source.height)
|
||||
return { left, top, width: right - left, height: bottom - top, reason: 'private-path' }
|
||||
})
|
||||
const regions = mergeRegions(proposedRegions)
|
||||
const targetRelative = `evidence/screenshots/public-redacted/${source.scenarioId}/${basename(source.file)}`
|
||||
const targetFile = join(inputRoot, targetRelative)
|
||||
if (relative(outputRoot, targetFile).startsWith('..')) throw new Error(`${targetRelative}: unsafe public screenshot target`)
|
||||
mkdirSync(dirname(targetFile), { recursive: true })
|
||||
if (regions.length === 0) cpSync(sourceFile, targetFile)
|
||||
else {
|
||||
await sharp(sourceFile)
|
||||
.composite(
|
||||
regions.map((region) => ({
|
||||
input: {
|
||||
create: {
|
||||
width: region.width,
|
||||
height: region.height,
|
||||
channels: 4,
|
||||
background: { r: 32, g: 34, b: 39, alpha: 1 },
|
||||
},
|
||||
},
|
||||
left: region.left,
|
||||
top: region.top,
|
||||
}))
|
||||
)
|
||||
.png()
|
||||
.toFile(targetFile)
|
||||
redactedScreenshots += 1
|
||||
redactionRegions += regions.length
|
||||
}
|
||||
publicByFile.set(source.file, {
|
||||
file: targetRelative,
|
||||
sha256: sha256(readFileSync(targetFile)),
|
||||
width: source.width,
|
||||
height: source.height,
|
||||
sourceSha256: source.sha256,
|
||||
redactions: regions,
|
||||
})
|
||||
}
|
||||
|
||||
const publicPlan = {
|
||||
schemaVersion: 'solver.public-screenshot-plan/v1',
|
||||
generatedAt: new Date().toISOString(),
|
||||
sourcePlanSha256: sha256(canonical(sourcePlan)),
|
||||
derivation: {
|
||||
method: 'Apple Vision exact-line private-path masking',
|
||||
redactedScreenshots,
|
||||
redactionRegions,
|
||||
},
|
||||
scenarios: sourcePlan.scenarios.map((scenario) => ({
|
||||
...scenario,
|
||||
screenshots: scenario.screenshots.map((screenshot) => publicByFile.get(screenshot.file)),
|
||||
})),
|
||||
}
|
||||
writeFileSync(outputPlanFile, `${JSON.stringify(publicPlan, null, 2)}\n`)
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
outputPlanFile,
|
||||
screenshots: sourceScreenshots.length,
|
||||
redactedScreenshots,
|
||||
redactionRegions,
|
||||
bytes: sourceScreenshots.reduce(
|
||||
(total, screenshot) => total + statSync(join(inputRoot, publicByFile.get(screenshot.file).file)).size,
|
||||
0
|
||||
),
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
257
web/solver-report/scripts/public-release-policy.mjs
Normal file
257
web/solver-report/scripts/public-release-policy.mjs
Normal file
@@ -0,0 +1,257 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
||||
import { dirname, isAbsolute, join, posix, relative, resolve } from 'node:path'
|
||||
|
||||
export const sha256 = (value) => createHash('sha256').update(value).digest('hex')
|
||||
|
||||
export const canonical = (value) => {
|
||||
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`
|
||||
if (value && typeof value === 'object') {
|
||||
return `{${Object.keys(value)
|
||||
.sort()
|
||||
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
||||
.join(',')}}`
|
||||
}
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
const isDescendant = (parent, candidate) => {
|
||||
const path = relative(parent, candidate)
|
||||
return path !== '' && !path.startsWith('..') && !isAbsolute(path)
|
||||
}
|
||||
|
||||
const overlaps = (left, right) =>
|
||||
left === right || isDescendant(left, right) || isDescendant(right, left)
|
||||
|
||||
const resolvePhysicalPath = (candidate) => {
|
||||
let ancestor = resolve(candidate)
|
||||
while (!existsSync(ancestor)) {
|
||||
const parent = dirname(ancestor)
|
||||
if (parent === ancestor) throw new Error(`cannot resolve an existing ancestor for ${candidate}`)
|
||||
ancestor = parent
|
||||
}
|
||||
return resolve(realpathSync(ancestor), relative(ancestor, resolve(candidate)))
|
||||
}
|
||||
|
||||
export function resolveSafeOutputRoot({ inputRoot, codeRoot, appRoot, requestedOutputRoot }) {
|
||||
const roots = {
|
||||
inputRoot: resolvePhysicalPath(inputRoot),
|
||||
codeRoot: resolvePhysicalPath(codeRoot),
|
||||
appRoot: resolvePhysicalPath(appRoot),
|
||||
outputRoot: resolvePhysicalPath(requestedOutputRoot),
|
||||
}
|
||||
const lexicalInputRoot = resolve(inputRoot)
|
||||
const lexicalOutputRoot = resolve(requestedOutputRoot)
|
||||
const lexicalManagedParent = join(lexicalInputRoot, 'report-web')
|
||||
const physicalManagedParent = resolvePhysicalPath(lexicalManagedParent)
|
||||
if (
|
||||
!isDescendant(lexicalManagedParent, lexicalOutputRoot) ||
|
||||
!isDescendant(roots.inputRoot, physicalManagedParent) ||
|
||||
!isDescendant(physicalManagedParent, roots.outputRoot)
|
||||
) {
|
||||
throw new Error('SOLVER_REPORT_OUTPUT_ROOT must be a child of <SOLVER_REPORT_INPUT_ROOT>/report-web')
|
||||
}
|
||||
if (roots.outputRoot === roots.inputRoot || isDescendant(roots.outputRoot, roots.inputRoot)) {
|
||||
throw new Error('report output must not equal or contain the evidence input root')
|
||||
}
|
||||
for (const [name, protectedRoot] of [
|
||||
['Agent source', roots.codeRoot],
|
||||
['DesireCore application', roots.appRoot],
|
||||
]) {
|
||||
if (overlaps(roots.outputRoot, protectedRoot)) {
|
||||
throw new Error(`report output must not overlap ${name} root`)
|
||||
}
|
||||
}
|
||||
return roots.outputRoot
|
||||
}
|
||||
|
||||
export function resolveSafeRemoteEntryUrl(baseUrl, entryPath) {
|
||||
if (
|
||||
typeof entryPath !== 'string' ||
|
||||
entryPath.length === 0 ||
|
||||
entryPath.startsWith('/') ||
|
||||
entryPath.includes('\\') ||
|
||||
entryPath.includes('%') ||
|
||||
entryPath.includes('?') ||
|
||||
entryPath.includes('#') ||
|
||||
entryPath.split('/').some((segment) => segment === '' || segment === '.' || segment === '..') ||
|
||||
posix.normalize(entryPath) !== entryPath ||
|
||||
/^[A-Za-z][A-Za-z0-9+.-]*:/.test(entryPath)
|
||||
) {
|
||||
throw new Error(`unsafe integrity entry path: ${String(entryPath)}`)
|
||||
}
|
||||
const base = new URL(baseUrl)
|
||||
if (!['http:', 'https:'].includes(base.protocol)) throw new Error('report base URL must use HTTP or HTTPS')
|
||||
const basePath = base.pathname.endsWith('/') ? base.pathname : `${base.pathname}/`
|
||||
const resolved = new URL(entryPath, base)
|
||||
if (resolved.origin !== base.origin || !resolved.pathname.startsWith(basePath)) {
|
||||
throw new Error(`integrity entry escapes report base URL: ${entryPath}`)
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
const PUBLIC_PROVENANCE_KEYS = new Set([
|
||||
'adapter_id',
|
||||
'capability_checked_at',
|
||||
'capability_hash',
|
||||
'compiled_request_hash',
|
||||
'compiler_id',
|
||||
'compiler_version',
|
||||
'connector_id',
|
||||
'decision_grade',
|
||||
'engine_id',
|
||||
'governance_mode',
|
||||
'ir_family',
|
||||
'ir_version',
|
||||
'job_id',
|
||||
'optimization_spec_hash',
|
||||
'problem_family',
|
||||
'request_id',
|
||||
'result_payload_hash',
|
||||
'validation_input_hash',
|
||||
'validation_report_hash',
|
||||
])
|
||||
|
||||
export function projectPublicProvenance(provenance) {
|
||||
if (!provenance || typeof provenance !== 'object' || Array.isArray(provenance)) return null
|
||||
return Object.fromEntries(
|
||||
Object.entries(provenance).filter(
|
||||
([key, value]) =>
|
||||
PUBLIC_PROVENANCE_KEYS.has(key) &&
|
||||
(value === null || ['string', 'number', 'boolean'].includes(typeof value))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const PUBLIC_TEXT_RULES = [
|
||||
['private-path', /(?:\/Users\/|\/Volumes\/|[A-Za-z]:\\Users\\)/i],
|
||||
['customer-codename', /(?:贝壳|beike|bei-ke)/i],
|
||||
['email-address', /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i],
|
||||
['private-key', /-----BEGIN (?:RSA |OPENSSH |EC )?PRIVATE KEY-----/i],
|
||||
['provider-secret', /\b(?:sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{12,}|xox[baprs]-[A-Za-z0-9-]{12,})\b/],
|
||||
['bearer-token', /\bBearer\s+[A-Za-z0-9._~+/-]{12,}={0,2}\b/i],
|
||||
[
|
||||
'named-secret',
|
||||
/\b(?:password|passwd|authorization|api[_ -]?key|access[_ -]?token|refresh[_ -]?token|client[_ -]?secret)\b\s*[:=]\s*["']?[^\s"',}]{6,}/i,
|
||||
],
|
||||
[
|
||||
'private-url',
|
||||
/https?:\/\/(?:localhost|127(?:\.\d{1,3}){3}|10(?:\.\d{1,3}){3}|192\.168(?:\.\d{1,3}){2}|172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2}|[^/\s"']+\.internal)\b/i,
|
||||
],
|
||||
]
|
||||
|
||||
export function publicTextViolations(value) {
|
||||
const text = (typeof value === 'string' ? value : JSON.stringify(value)).replace(
|
||||
/\b\d+@2x\.(?:png|webp)\b/gi,
|
||||
'[reviewed-retina-image]'
|
||||
)
|
||||
return PUBLIC_TEXT_RULES.filter(([, pattern]) => pattern.test(text)).map(([rule]) => rule)
|
||||
}
|
||||
|
||||
export function assertPublicText(value, label) {
|
||||
const violations = publicTextViolations(value)
|
||||
if (violations.length > 0) {
|
||||
throw new Error(`${label}: public-release text policy rejected ${violations.join(',')}`)
|
||||
}
|
||||
}
|
||||
|
||||
const readJsonLines = (file) =>
|
||||
readFileSync(file, 'utf8')
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line))
|
||||
|
||||
export function computePublicEvidenceRoots({
|
||||
inputRoot,
|
||||
screenshotPlan,
|
||||
screenshotValidation,
|
||||
screenshotPrivacyValidation,
|
||||
caseRunById,
|
||||
}) {
|
||||
const transcriptEntries = screenshotPlan.scenarios.map((item) => {
|
||||
const source = caseRunById.get(item.runId)
|
||||
if (!source) throw new Error(`${item.scenarioId}: source Run missing for public attestation`)
|
||||
const messages = readJsonLines(join(source.runDir, 'messages.jsonl'))
|
||||
.filter((message) => ['user', 'assistant'].includes(message.role))
|
||||
.map((message) => ({ role: message.role, content: message.content ?? '', timestamp: message.timestamp ?? null }))
|
||||
return { scenarioId: item.scenarioId, runId: item.runId, messages }
|
||||
})
|
||||
const screenshotEntries = screenshotPlan.scenarios.flatMap((item) =>
|
||||
item.screenshots.map((screenshot) => {
|
||||
const file = join(inputRoot, screenshot.file)
|
||||
if (!existsSync(file)) throw new Error(`${item.scenarioId}: screenshot missing: ${screenshot.file}`)
|
||||
const actualSha256 = sha256(readFileSync(file))
|
||||
if (actualSha256 !== screenshot.sha256) {
|
||||
throw new Error(`${item.scenarioId}: screenshot hash differs from reviewed plan: ${screenshot.file}`)
|
||||
}
|
||||
return {
|
||||
scenarioId: item.scenarioId,
|
||||
file: screenshot.file,
|
||||
sha256: screenshot.sha256,
|
||||
width: screenshot.width,
|
||||
height: screenshot.height,
|
||||
}
|
||||
})
|
||||
)
|
||||
const screenshotPlanSha256 = sha256(canonical(screenshotPlan))
|
||||
const publicScreenshotContentRootSha256 = sha256(canonical(screenshotEntries))
|
||||
if (
|
||||
screenshotPrivacyValidation?.schemaVersion !== 'solver.screenshot-privacy-validation/v1' ||
|
||||
screenshotPrivacyValidation.passed !== true ||
|
||||
screenshotPrivacyValidation.screenshots !== screenshotEntries.length ||
|
||||
screenshotPrivacyValidation.screenshotPlanSha256 !== screenshotPlanSha256 ||
|
||||
screenshotPrivacyValidation.screenshotContentRootSha256 !== publicScreenshotContentRootSha256 ||
|
||||
screenshotPrivacyValidation.ocr?.screenshotsWithText !== screenshotEntries.length ||
|
||||
screenshotPrivacyValidation.ocr?.sensitiveMatches !== 0
|
||||
) {
|
||||
throw new Error('public screenshot privacy validation is missing or differs from current public images')
|
||||
}
|
||||
return {
|
||||
publicTranscriptContentRootSha256: sha256(canonical(transcriptEntries)),
|
||||
publicScreenshotContentRootSha256,
|
||||
screenshotPlanSha256,
|
||||
screenshotValidationSha256: sha256(canonical(screenshotValidation)),
|
||||
screenshotPrivacyValidationSha256: sha256(canonical(screenshotPrivacyValidation)),
|
||||
}
|
||||
}
|
||||
|
||||
const HASH_PATTERN = /^[a-f0-9]{64}$/
|
||||
|
||||
export function validatePublicReleaseAttestation(attestation, evidenceRoots) {
|
||||
if (attestation?.schemaVersion !== 'solver.public-release-attestation/v1') {
|
||||
throw new Error('public release attestation is missing or has an unsupported schema')
|
||||
}
|
||||
if (attestation.audience !== 'public' || attestation.review?.status !== 'passed') {
|
||||
throw new Error('public release attestation must explicitly pass for a public audience')
|
||||
}
|
||||
const assertions = attestation.review?.assertions ?? {}
|
||||
for (const key of ['transcriptsContainNoSensitiveData', 'screenshotsContainNoSensitiveData', 'customerIdentifiersExcluded']) {
|
||||
if (assertions[key] !== true) throw new Error(`public release attestation assertion is not true: ${key}`)
|
||||
}
|
||||
if (!attestation.review?.reviewedAt || !attestation.review?.reviewer || !Array.isArray(attestation.review?.methods)) {
|
||||
throw new Error('public release attestation review identity, time, and methods are required')
|
||||
}
|
||||
for (const [key, expected] of Object.entries(evidenceRoots)) {
|
||||
const actual = attestation.evidence?.[key]
|
||||
if (!HASH_PATTERN.test(actual ?? '') || actual !== expected) {
|
||||
throw new Error(`public release attestation does not match current evidence: ${key}`)
|
||||
}
|
||||
}
|
||||
const metadata = attestation.metadata ?? {}
|
||||
if (
|
||||
typeof metadata.platformCommit !== 'string' ||
|
||||
typeof metadata.agentSkillVersion !== 'string' ||
|
||||
!Number.isInteger(metadata.representativeStabilityPasses) ||
|
||||
metadata.representativeStabilityPasses < 0
|
||||
) {
|
||||
throw new Error('public release attestation metadata is incomplete')
|
||||
}
|
||||
return attestation
|
||||
}
|
||||
|
||||
export function loadPublicReleaseAttestation(file, evidenceRoots) {
|
||||
if (!existsSync(file)) throw new Error(`public release attestation is required: ${file}`)
|
||||
const bytes = readFileSync(file)
|
||||
const attestation = validatePublicReleaseAttestation(JSON.parse(bytes), evidenceRoots)
|
||||
return { attestation, sha256: sha256(bytes) }
|
||||
}
|
||||
100
web/solver-report/scripts/validate-deployed-report.mjs
Normal file
100
web/solver-report/scripts/validate-deployed-report.mjs
Normal file
@@ -0,0 +1,100 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { canonical, resolveSafeRemoteEntryUrl } from './public-release-policy.mjs'
|
||||
|
||||
const baseUrl = (process.env.REPORT_BASE_URL ?? 'https://build.desirecore.cc/solver-agent-team/').replace(/\/?$/, '/')
|
||||
const inputRoot = process.env.SOLVER_REPORT_INPUT_ROOT ?? process.cwd()
|
||||
const validationFile =
|
||||
process.env.SOLVER_REPORT_DEPLOYMENT_VALIDATION_FILE ?? join(inputRoot, 'report-web', 'deployment-validation.json')
|
||||
|
||||
async function fetchWithoutRedirect(url, init = {}) {
|
||||
const response = await fetch(url, { ...init, redirect: 'manual' })
|
||||
if (response.status >= 300 && response.status < 400) {
|
||||
throw new Error(`redirect-rejected:${response.status}`)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
const integrityResponse = await fetchWithoutRedirect(new URL('integrity.json', baseUrl))
|
||||
if (!integrityResponse.ok) throw new Error(`integrity.json: ${integrityResponse.status}`)
|
||||
const integrity = await integrityResponse.json()
|
||||
const failures = []
|
||||
let checked = 0
|
||||
let checkedBytes = 0
|
||||
let cursor = 0
|
||||
|
||||
async function worker() {
|
||||
while (cursor < integrity.entries.length) {
|
||||
const index = cursor++
|
||||
const entry = integrity.entries[index]
|
||||
try {
|
||||
const entryUrl = resolveSafeRemoteEntryUrl(baseUrl, entry.path)
|
||||
const response = await fetchWithoutRedirect(entryUrl)
|
||||
if (!response.ok) {
|
||||
failures.push({ path: entry.path, issue: `http-${response.status}` })
|
||||
continue
|
||||
}
|
||||
const bytes = Buffer.from(await response.arrayBuffer())
|
||||
const digest = createHash('sha256').update(bytes).digest('hex')
|
||||
if (bytes.length !== entry.bytes || digest !== entry.sha256) {
|
||||
failures.push({ path: entry.path, issue: 'integrity-mismatch' })
|
||||
}
|
||||
checked += 1
|
||||
checkedBytes += bytes.length
|
||||
} catch (error) {
|
||||
failures.push({ path: entry.path, issue: error instanceof Error ? error.message : String(error) })
|
||||
}
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from({ length: 12 }, () => worker()))
|
||||
|
||||
const rootResponse = await fetchWithoutRedirect(baseUrl)
|
||||
const mediaResponse = await fetchWithoutRedirect(new URL('media/original/boundary.qp-unsupported/01@2x.png', baseUrl), {
|
||||
method: 'HEAD',
|
||||
})
|
||||
const [buildResponse, manifestResponse] = await Promise.all([
|
||||
fetchWithoutRedirect(new URL('build.json', baseUrl)),
|
||||
fetchWithoutRedirect(new URL('data/manifest.json', baseUrl)),
|
||||
])
|
||||
const build = buildResponse.ok ? await buildResponse.json() : null
|
||||
const manifest = manifestResponse.ok ? await manifestResponse.json() : null
|
||||
if (build?.gates?.publicReleasePrivacy !== 'pass') failures.push({ path: 'build.json', issue: 'public-release-privacy-gate' })
|
||||
if (
|
||||
!build?.publicReleaseAttestationSha256 ||
|
||||
build.publicReleaseAttestationSha256 !== manifest?.publicReleaseAttestationSha256
|
||||
)
|
||||
failures.push({ path: 'build.json', issue: 'public-release-attestation-mismatch' })
|
||||
if (
|
||||
!build ||
|
||||
!manifest ||
|
||||
build.buildId !== manifest.buildId ||
|
||||
build.contentRootSha256 !== manifest.contentRootSha256 ||
|
||||
integrity.contentRootSha256 !== manifest.contentRootSha256 ||
|
||||
canonical(build.publicEvidenceRoots) !== canonical(manifest.publicEvidenceRoots) ||
|
||||
canonical(build.counts) !== canonical(manifest.counts) ||
|
||||
build.integrityEntries !== integrity.entries.length ||
|
||||
!integrity.entries.some((entry) => entry.path === 'build.json')
|
||||
)
|
||||
failures.push({ path: 'build.json', issue: 'build-manifest-integrity-projection' })
|
||||
const report = {
|
||||
schemaVersion: 'solver.web-deployment-validation/v1',
|
||||
testedAt: new Date().toISOString(), baseUrl,
|
||||
contentRootSha256: integrity.contentRootSha256,
|
||||
checkedEntries: checked, checkedBytes,
|
||||
rootStatus: rootResponse.status,
|
||||
csp: rootResponse.headers.get('content-security-policy'),
|
||||
mediaStatus: mediaResponse.status,
|
||||
mediaCacheControl: mediaResponse.headers.get('cache-control'),
|
||||
failures,
|
||||
passed:
|
||||
failures.length === 0 &&
|
||||
checked === integrity.entries.length &&
|
||||
rootResponse.ok &&
|
||||
mediaResponse.ok &&
|
||||
buildResponse.ok &&
|
||||
manifestResponse.ok,
|
||||
}
|
||||
writeFileSync(validationFile, `${JSON.stringify(report, null, 2)}\n`)
|
||||
console.log(JSON.stringify(report, null, 2))
|
||||
if (!report.passed) process.exitCode = 1
|
||||
238
web/solver-report/scripts/validate-report-web.mjs
Normal file
238
web/solver-report/scripts/validate-report-web.mjs
Normal file
@@ -0,0 +1,238 @@
|
||||
import { existsSync, globSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import {
|
||||
assertPublicText,
|
||||
canonical,
|
||||
computePublicEvidenceRoots,
|
||||
loadPublicReleaseAttestation,
|
||||
sha256,
|
||||
} from './public-release-policy.mjs'
|
||||
|
||||
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 =
|
||||
process.env.SOLVER_REPORT_VALIDATION_FILE ?? join(inputRoot, 'report-web', 'validation.json')
|
||||
const manifest = JSON.parse(readFileSync(join(dist, 'data', 'manifest.json'), 'utf8'))
|
||||
const integrity = JSON.parse(readFileSync(join(dist, 'integrity.json'), 'utf8'))
|
||||
const build = JSON.parse(readFileSync(join(dist, 'build.json'), 'utf8'))
|
||||
const latestRegression = JSON.parse(readFileSync(join(dist, 'data', 'latest-platform-regression.json'), 'utf8'))
|
||||
const failures = []
|
||||
const readJsonLines = (file) =>
|
||||
readFileSync(file, 'utf8')
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line))
|
||||
|
||||
const caseRunById = new Map(
|
||||
globSync(join(inputRoot, 'evidence', '**', 'case-run.json')).map((file) => {
|
||||
const data = JSON.parse(readFileSync(file, 'utf8'))
|
||||
return [data.runId, { file, data, runDir: join(dirname(file), 'run') }]
|
||||
})
|
||||
)
|
||||
const screenshotPlan = JSON.parse(
|
||||
readFileSync(
|
||||
process.env.SOLVER_REPORT_PUBLIC_SCREENSHOT_PLAN_FILE ??
|
||||
join(inputRoot, 'evidence', 'screenshots', 'public-screenshot-plan.json'),
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
const screenshotValidation = JSON.parse(
|
||||
readFileSync(join(inputRoot, 'evidence', 'screenshots', 'screenshot-validation.json'), 'utf8')
|
||||
)
|
||||
const screenshotPrivacyValidation = JSON.parse(
|
||||
readFileSync(
|
||||
process.env.SOLVER_REPORT_SCREENSHOT_PRIVACY_VALIDATION_FILE ??
|
||||
join(inputRoot, 'evidence', 'screenshots', 'public-screenshot-privacy-validation.json'),
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
const publicEvidenceRoots = computePublicEvidenceRoots({
|
||||
inputRoot,
|
||||
screenshotPlan,
|
||||
screenshotValidation,
|
||||
screenshotPrivacyValidation,
|
||||
caseRunById,
|
||||
})
|
||||
const publicAttestationFile =
|
||||
process.env.SOLVER_REPORT_PUBLIC_RELEASE_ATTESTATION_FILE ??
|
||||
join(inputRoot, 'evidence', 'public-release-attestation.json')
|
||||
const { attestation: publicReleaseAttestation, sha256: publicReleaseAttestationSha256 } =
|
||||
loadPublicReleaseAttestation(publicAttestationFile, publicEvidenceRoots)
|
||||
|
||||
if (manifest.chapters.length !== 24) failures.push({ issue: 'chapter-count', actual: manifest.chapters.length })
|
||||
if (manifest.counts.screenshots !== 174)
|
||||
failures.push({ issue: 'screenshot-count', actual: manifest.counts.screenshots })
|
||||
const historicalEvidenceContentRootSha256 = sha256(
|
||||
canonical(manifest.chapters.map((item) => ({ scenarioId: item.scenarioId, sha256: item.sha256 })))
|
||||
)
|
||||
const latestPlatformRegressionSha256 = sha256(canonical(latestRegression))
|
||||
const expectedContentRootSha256 = sha256(
|
||||
canonical({ historicalEvidenceContentRootSha256, latestPlatformRegressionSha256 })
|
||||
)
|
||||
if (manifest.historicalEvidenceContentRootSha256 !== historicalEvidenceContentRootSha256)
|
||||
failures.push({ issue: 'historical-evidence-root' })
|
||||
if (manifest.latestPlatformRegressionSha256 !== latestPlatformRegressionSha256)
|
||||
failures.push({ issue: 'latest-regression-hash' })
|
||||
if (
|
||||
manifest.contentRootSha256 !== expectedContentRootSha256 ||
|
||||
integrity.contentRootSha256 !== expectedContentRootSha256
|
||||
)
|
||||
failures.push({ issue: 'report-content-root' })
|
||||
if (latestRegression.schemaVersion !== 'solver.latest-platform-regression/v1' || latestRegression.passed !== true)
|
||||
failures.push({ issue: 'latest-regression-status' })
|
||||
if (manifest.latestPlatformRegression?.platform?.commit !== latestRegression.platform?.commit)
|
||||
failures.push({ issue: 'latest-regression-manifest-projection' })
|
||||
if (
|
||||
!manifest.evidencePlatformCommit?.startsWith(publicReleaseAttestation.metadata.platformCommit) ||
|
||||
!manifest.evidencePlatformCommit?.startsWith(latestRegression.historicalEvidence?.platformCommit)
|
||||
)
|
||||
failures.push({ issue: 'historical-platform-attribution' })
|
||||
if (
|
||||
manifest.platformCommit !== latestRegression.platform?.commit ||
|
||||
manifest.agentSkillVersion !== latestRegression.platform?.agentSkillVersion ||
|
||||
manifest.counts.representativeStabilityPasses !== publicReleaseAttestation.metadata.representativeStabilityPasses
|
||||
)
|
||||
failures.push({ issue: 'derived-release-metadata' })
|
||||
if (
|
||||
manifest.publicReleaseAttestationSha256 !== publicReleaseAttestationSha256 ||
|
||||
canonical(manifest.publicEvidenceRoots) !== canonical(publicEvidenceRoots)
|
||||
)
|
||||
failures.push({ issue: 'public-release-attestation-projection' })
|
||||
if (
|
||||
build.buildId !== manifest.buildId ||
|
||||
build.contentRootSha256 !== manifest.contentRootSha256 ||
|
||||
build.publicReleaseAttestationSha256 !== manifest.publicReleaseAttestationSha256 ||
|
||||
canonical(build.publicEvidenceRoots) !== canonical(manifest.publicEvidenceRoots) ||
|
||||
canonical(build.counts) !== canonical(manifest.counts) ||
|
||||
build.integrityEntries !== integrity.entries.length ||
|
||||
build.gates?.publicReleasePrivacy !== 'pass' ||
|
||||
!integrity.entries.some((entry) => entry.path === 'build.json')
|
||||
)
|
||||
failures.push({ issue: 'build-manifest-integrity-projection' })
|
||||
try {
|
||||
assertPublicText(latestRegression, 'latest platform regression')
|
||||
} catch (error) {
|
||||
failures.push({ issue: 'latest-regression-public-text-policy', detail: error.message })
|
||||
}
|
||||
|
||||
let formulas = 0
|
||||
let screenshotCount = 0
|
||||
for (const index of manifest.chapters) {
|
||||
const file = join(dist, index.json)
|
||||
const script = join(dist, index.script)
|
||||
if (!existsSync(file) || !existsSync(script)) {
|
||||
failures.push({ scenarioId: index.scenarioId, issue: 'chapter-file-missing' })
|
||||
continue
|
||||
}
|
||||
const chapter = JSON.parse(readFileSync(file, 'utf8'))
|
||||
const withoutHash = structuredClone(chapter)
|
||||
delete withoutHash.contentSha256
|
||||
if (sha256(canonical(withoutHash)) !== chapter.contentSha256 || chapter.contentSha256 !== index.sha256) {
|
||||
failures.push({ scenarioId: index.scenarioId, issue: 'chapter-hash' })
|
||||
}
|
||||
if (chapter.sections.length !== 10 || chapter.sections.some((section, i) => section.number !== i + 1)) {
|
||||
failures.push({ scenarioId: index.scenarioId, issue: 'ten-part-report' })
|
||||
}
|
||||
if (chapter.formulaStats.source !== chapter.formulaStats.rendered) {
|
||||
failures.push({ scenarioId: index.scenarioId, issue: 'formula-count', stats: chapter.formulaStats })
|
||||
}
|
||||
const renderedHtml = [
|
||||
...chapter.timeline.filter((item) => item.kind === 'message').map((item) => item.html),
|
||||
...chapter.sections.map((item) => item.html),
|
||||
].join('\n')
|
||||
const mathmlCount = (renderedHtml.match(/<math\b/g) ?? []).length
|
||||
if (mathmlCount !== chapter.formulaStats.rendered || /katex-error/.test(renderedHtml)) {
|
||||
failures.push({
|
||||
scenarioId: index.scenarioId,
|
||||
issue: 'mathml-render',
|
||||
expected: chapter.formulaStats.rendered,
|
||||
actual: mathmlCount,
|
||||
})
|
||||
}
|
||||
const renderedWithoutCode = renderedHtml.replace(/<pre[\s\S]*?<\/pre>/g, '').replace(/<code[\s\S]*?<\/code>/g, '')
|
||||
if (/\\\(|\\\)|\\\[|\\\]/.test(renderedWithoutCode)) {
|
||||
failures.push({ scenarioId: index.scenarioId, issue: 'unrendered-latex-delimiter' })
|
||||
}
|
||||
formulas += chapter.formulaStats.rendered
|
||||
screenshotCount += chapter.media.length
|
||||
for (const image of chapter.media) {
|
||||
const original = join(dist, image.original)
|
||||
if (!existsSync(original) || sha256(readFileSync(original)) !== image.sha256) {
|
||||
failures.push({ scenarioId: index.scenarioId, issue: 'original-image', path: image.original })
|
||||
}
|
||||
for (const thumb of [image.thumb756, image.thumb1512]) {
|
||||
if (!existsSync(join(dist, thumb)))
|
||||
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) =>
|
||||
['user', 'assistant'].includes(item.role)
|
||||
)
|
||||
const projected = chapter.timeline.filter((item) => item.kind === 'message')
|
||||
if (
|
||||
raw.length !== projected.length ||
|
||||
raw.some((message, i) => message.role !== projected[i].role || (message.content ?? '') !== projected[i].markdown)
|
||||
) {
|
||||
failures.push({ scenarioId: index.scenarioId, issue: 'transcript-not-1-to-1' })
|
||||
}
|
||||
}
|
||||
const serialized = JSON.stringify(chapter)
|
||||
try {
|
||||
assertPublicText(serialized, `${index.scenarioId} chapter`)
|
||||
} catch (error) {
|
||||
failures.push({ scenarioId: index.scenarioId, issue: 'public-text-policy', detail: error.message })
|
||||
}
|
||||
if (
|
||||
['qp', 'qcp', 'cp'].includes(String(chapter.problemFamily).toLowerCase()) &&
|
||||
/当前只支持\s*LP\/?MILP/i.test(serialized)
|
||||
) {
|
||||
failures.push({ scenarioId: index.scenarioId, issue: 'stale-capability-summary' })
|
||||
}
|
||||
}
|
||||
|
||||
if (screenshotCount !== 174) failures.push({ issue: 'chapter-screenshot-sum', actual: screenshotCount })
|
||||
for (const entry of integrity.entries) {
|
||||
const file = join(dist, entry.path)
|
||||
if (!existsSync(file)) failures.push({ issue: 'integrity-file-missing', path: entry.path })
|
||||
else {
|
||||
const bytes = readFileSync(file)
|
||||
if (bytes.length !== entry.bytes || sha256(bytes) !== entry.sha256)
|
||||
failures.push({ issue: 'integrity-mismatch', path: entry.path })
|
||||
}
|
||||
}
|
||||
for (const required of [
|
||||
'index.html',
|
||||
'assets/report.css',
|
||||
'assets/report.js',
|
||||
'assets/katex.min.css',
|
||||
'data/manifest.js',
|
||||
'data/latest-platform-regression.json',
|
||||
]) {
|
||||
if (!existsSync(join(dist, required))) failures.push({ issue: 'required-asset', path: required })
|
||||
}
|
||||
|
||||
const report = {
|
||||
schemaVersion: 'solver.web-report-validation/v1',
|
||||
generatedAt: new Date().toISOString(),
|
||||
buildId: manifest.buildId,
|
||||
chapters: manifest.chapters.length,
|
||||
screenshots: screenshotCount,
|
||||
formulas,
|
||||
integrityEntries: integrity.entries.length,
|
||||
bytes: globSync(join(dist, '**', '*'))
|
||||
.filter((file) => statSync(file).isFile())
|
||||
.reduce((sum, file) => sum + statSync(file).size, 0),
|
||||
latestPlatformRegression: {
|
||||
commit: latestRegression.platform.commit,
|
||||
scenarios: latestRegression.runtime.scenarios.length,
|
||||
decisionWorkspace: latestRegression.runtime.decisionWorkspace.status,
|
||||
},
|
||||
passed: failures.length === 0,
|
||||
failures,
|
||||
}
|
||||
writeFileSync(validationFile, `${JSON.stringify(report, null, 2)}\n`)
|
||||
console.log(JSON.stringify(report, null, 2))
|
||||
if (!report.passed) process.exitCode = 1
|
||||
@@ -0,0 +1,80 @@
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { assertPublicText, canonical, sha256 } from './public-release-policy.mjs'
|
||||
|
||||
const inputRoot = resolve(process.env.SOLVER_REPORT_INPUT_ROOT ?? process.cwd())
|
||||
const planFile =
|
||||
process.env.SOLVER_REPORT_SCREENSHOT_PLAN_FILE ??
|
||||
join(inputRoot, 'evidence', 'screenshots', 'screenshot-plan.json')
|
||||
const ocrFile = process.env.SOLVER_REPORT_SCREENSHOT_OCR_FILE
|
||||
if (!ocrFile) throw new Error('SOLVER_REPORT_SCREENSHOT_OCR_FILE is required')
|
||||
const validationFile = process.env.SOLVER_REPORT_SCREENSHOT_OCR_VALIDATION_FILE
|
||||
if (!validationFile) throw new Error('SOLVER_REPORT_SCREENSHOT_OCR_VALIDATION_FILE is required')
|
||||
|
||||
const plan = JSON.parse(readFileSync(planFile, 'utf8'))
|
||||
const ocr = JSON.parse(readFileSync(ocrFile, 'utf8'))
|
||||
if (ocr.schemaVersion !== 'solver.screenshot-privacy-ocr/v1' || !Array.isArray(ocr.entries)) {
|
||||
throw new Error('unsupported screenshot OCR result')
|
||||
}
|
||||
|
||||
const planned = plan.scenarios.flatMap((scenario) =>
|
||||
scenario.screenshots.map((screenshot) => ({ scenarioId: scenario.scenarioId, ...screenshot }))
|
||||
)
|
||||
if (planned.length !== 174 || ocr.entries.length !== planned.length) {
|
||||
throw new Error(`screenshot OCR coverage mismatch: expected ${planned.length}, received ${ocr.entries.length}`)
|
||||
}
|
||||
|
||||
const failures = []
|
||||
const screenshotRootEntries = []
|
||||
let recognizedLines = 0
|
||||
let screenshotsWithText = 0
|
||||
for (const [index, expected] of planned.entries()) {
|
||||
const actual = ocr.entries[index]
|
||||
const imageFile = join(inputRoot, expected.file)
|
||||
if (!existsSync(imageFile)) failures.push({ file: expected.file, issue: 'missing-image' })
|
||||
const actualSha256 = existsSync(imageFile) ? sha256(readFileSync(imageFile)) : null
|
||||
if (
|
||||
actual?.scenarioId !== expected.scenarioId ||
|
||||
actual?.file !== expected.file ||
|
||||
actualSha256 !== expected.sha256 ||
|
||||
!Array.isArray(actual?.lines) ||
|
||||
actual.lines.some((line) => typeof line?.text !== 'string' || !Array.isArray(line?.boundingBox))
|
||||
) {
|
||||
failures.push({ file: expected.file, issue: 'plan-or-hash-mismatch' })
|
||||
continue
|
||||
}
|
||||
const text = actual.lines.map((line) => line.text).join('\n')
|
||||
recognizedLines += actual.lines.length
|
||||
if (actual.lines.length > 0) screenshotsWithText += 1
|
||||
try {
|
||||
assertPublicText(text, `${expected.scenarioId} screenshot OCR`)
|
||||
} catch (error) {
|
||||
failures.push({ file: expected.file, issue: 'sensitive-text-policy', detail: error.message })
|
||||
}
|
||||
screenshotRootEntries.push({
|
||||
scenarioId: expected.scenarioId,
|
||||
file: expected.file,
|
||||
sha256: expected.sha256,
|
||||
width: expected.width,
|
||||
height: expected.height,
|
||||
})
|
||||
}
|
||||
|
||||
const report = {
|
||||
schemaVersion: 'solver.screenshot-privacy-validation/v1',
|
||||
auditedAt: new Date().toISOString(),
|
||||
screenshotPlanSha256: sha256(canonical(plan)),
|
||||
screenshotContentRootSha256: sha256(canonical(screenshotRootEntries)),
|
||||
screenshots: planned.length,
|
||||
ocr: {
|
||||
engine: ocr.engine,
|
||||
screenshotsWithText,
|
||||
recognizedLines,
|
||||
sensitiveMatches: failures.filter((failure) => failure.issue === 'sensitive-text-policy').length,
|
||||
},
|
||||
passed: failures.length === 0 && screenshotsWithText === planned.length,
|
||||
failures,
|
||||
}
|
||||
writeFileSync(validationFile, `${JSON.stringify(report, null, 2)}\n`)
|
||||
console.log(JSON.stringify(report, null, 2))
|
||||
if (!report.passed) process.exitCode = 1
|
||||
64
web/solver-report/scripts/verify-static-release.py
Normal file
64
web/solver-report/scripts/verify-static-release.py
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
import hashlib
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
root = pathlib.Path(sys.argv[1]).resolve()
|
||||
integrity = json.loads((root / "integrity.json").read_text(encoding="utf-8"))
|
||||
failures = []
|
||||
for entry in integrity["entries"]:
|
||||
raw_path = entry.get("path")
|
||||
if (
|
||||
not isinstance(raw_path, str)
|
||||
or not raw_path
|
||||
or "//" in raw_path
|
||||
or "\\" in raw_path
|
||||
or "%" in raw_path
|
||||
or "?" in raw_path
|
||||
or "#" in raw_path
|
||||
):
|
||||
failures.append({"path": str(raw_path), "issue": "unsafe-path"})
|
||||
continue
|
||||
relative_path = pathlib.PurePosixPath(raw_path)
|
||||
if relative_path.is_absolute() or any(part in ("", ".", "..") for part in relative_path.parts):
|
||||
failures.append({"path": raw_path, "issue": "unsafe-path"})
|
||||
continue
|
||||
path = (root / pathlib.Path(*relative_path.parts)).resolve()
|
||||
if path == root or root not in path.parents:
|
||||
failures.append({"path": raw_path, "issue": "path-escape"})
|
||||
continue
|
||||
if not path.is_file():
|
||||
failures.append({"path": raw_path, "issue": "missing"})
|
||||
continue
|
||||
data = path.read_bytes()
|
||||
digest = hashlib.sha256(data).hexdigest()
|
||||
if len(data) != entry["bytes"] or digest != entry["sha256"]:
|
||||
failures.append({"path": raw_path, "issue": "mismatch"})
|
||||
|
||||
manifest = json.loads((root / "data" / "manifest.json").read_text(encoding="utf-8"))
|
||||
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("publicReleaseAttestationSha256") != manifest.get("publicReleaseAttestationSha256"):
|
||||
failures.append({"path": "build.json", "issue": "public-release-attestation-mismatch"})
|
||||
if (
|
||||
build.get("buildId") != manifest.get("buildId")
|
||||
or build.get("contentRootSha256") != manifest.get("contentRootSha256")
|
||||
or integrity.get("contentRootSha256") != manifest.get("contentRootSha256")
|
||||
or build.get("publicEvidenceRoots") != manifest.get("publicEvidenceRoots")
|
||||
or build.get("counts") != manifest.get("counts")
|
||||
or build.get("integrityEntries") != len(integrity.get("entries", []))
|
||||
or not any(entry.get("path") == "build.json" for entry in integrity.get("entries", []))
|
||||
):
|
||||
failures.append({"path": "build.json", "issue": "build-manifest-integrity-projection"})
|
||||
result = {
|
||||
"buildId": manifest["buildId"],
|
||||
"contentRootSha256": manifest["contentRootSha256"],
|
||||
"chapters": len(manifest["chapters"]),
|
||||
"integrityEntries": len(integrity["entries"]),
|
||||
"failures": failures,
|
||||
"passed": not failures,
|
||||
}
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
raise SystemExit(0 if result["passed"] else 1)
|
||||
Reference in New Issue
Block a user