139 lines
9.0 KiB
JavaScript
139 lines
9.0 KiB
JavaScript
"use strict";
|
|
|
|
const { ANALYZER_VERSION, analyzeBundle } = require("./analyzer");
|
|
const { canonicalJson } = require("./cache");
|
|
const { DIMENSIONS, PILOT_LIMITS, PROMPT_VERSION, REVIEWER_VERSION, RUBRIC_VERSION, SCHEMA_VERSION, SEMANTIC_REVIEWER } = require("./constants");
|
|
const { verifyJudgment } = require("./evidence");
|
|
const { buildPacket, packetHash, secretLike } = require("./packet");
|
|
const { aggregateScore, weightedJudgeScore } = require("./score");
|
|
const { bundleMap } = require("./safe-bundle");
|
|
const { deterministicValidation, splitFrontmatter, tesslAlignedValidation } = require("./validation");
|
|
const { productionTriage } = require("./triage");
|
|
|
|
function requiredString(value, label, maxBytes = 4096) {
|
|
if (typeof value !== "string" || !value.trim() || Buffer.byteLength(value, "utf8") > maxBytes) throw new Error(`${label} must be a bounded non-empty string`);
|
|
}
|
|
|
|
function assertKeys(value, expected, label) {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
const actual = Object.keys(value).sort();
|
|
if (canonicalJson(actual) !== canonicalJson([...expected].sort())) throw new Error(`${label} has unexpected properties`);
|
|
}
|
|
|
|
function expectedVersions() {
|
|
return { analyzer: ANALYZER_VERSION, prompt: PROMPT_VERSION, reviewer: REVIEWER_VERSION, rubric: RUBRIC_VERSION, schema: SCHEMA_VERSION };
|
|
}
|
|
|
|
function sanitizeJudgments(judgments) {
|
|
return Object.fromEntries(Object.entries(judgments).map(([kind, judgment]) => [kind, {
|
|
...judgment,
|
|
dimensions: Object.fromEntries(Object.entries(judgment.dimensions).map(([name, dimension]) => [name, {
|
|
...dimension,
|
|
evidence: dimension.evidence.map((item) => secretLike(item.excerpt)
|
|
? { path: item.path, start_line: item.start_line, end_line: item.end_line, redacted: true, excerpt_sha256: require("crypto").createHash("sha256").update(item.excerpt).digest("hex") }
|
|
: item),
|
|
}])),
|
|
}]));
|
|
}
|
|
|
|
function buildResult(bundle, key) {
|
|
const validation = tesslAlignedValidation(bundle, bundle.skillId);
|
|
const aasValidation = deterministicValidation(bundle.files[0].text, bundle.skillId);
|
|
const rawJudgments = analyzeBundle(bundle);
|
|
verifyJudgment("description", rawJudgments.description, bundleMap(bundle), DIMENSIONS.description);
|
|
verifyJudgment("content", rawJudgments.content, bundleMap(bundle), DIMENSIONS.content);
|
|
const descriptionScore = weightedJudgeScore("description", rawJudgments.description.dimensions);
|
|
const contentScore = weightedJudgeScore("content", rawJudgments.content.dimensions);
|
|
const judgments = sanitizeJudgments(rawJudgments);
|
|
const risk = splitFrontmatter(bundle.files[0].text).metadata?.risk || "unknown";
|
|
const result = {
|
|
status: "completed",
|
|
cacheKey: key,
|
|
cacheHit: false,
|
|
skillId: bundle.skillId,
|
|
skillPath: bundle.skillPath,
|
|
bundleHash: bundle.bundleHash,
|
|
files: bundle.files.map(({ path, sha256, size, encoding }) => ({ path, sha256, size, encoding })),
|
|
local_quality_score: aggregateScore(validation.score, descriptionScore, contentScore),
|
|
risk,
|
|
components: { validation, description: descriptionScore, content: contentScore },
|
|
judgments,
|
|
aas_policy: { status: aasValidation.score === 1 ? "pass" : "needs_review", findings: aasValidation.checks.filter((item) => !item.passed), validation: aasValidation },
|
|
confidence: Object.fromEntries(Object.entries(judgments).map(([kind, value]) => [kind, Object.values(value.dimensions).reduce((sum, item) => sum + item.confidence, 0) / Object.keys(value.dimensions).length])),
|
|
versions: expectedVersions(),
|
|
};
|
|
result.triage = productionTriage(result);
|
|
return result;
|
|
}
|
|
|
|
function validateResult(result, bundle, key) {
|
|
const expected = buildResult(bundle, key);
|
|
if (canonicalJson(result) !== canonicalJson(expected)) throw new Error("Result differs from canonical deterministic result");
|
|
return true;
|
|
}
|
|
|
|
function validateRuntimeResult(result, bundle, key) {
|
|
if (typeof result?.cacheHit !== "boolean") throw new Error("Runtime result cache metadata invalid");
|
|
const canonical = result.cacheHit ? { ...result, cacheHit: false } : result;
|
|
validateResult(canonical, bundle, key);
|
|
return true;
|
|
}
|
|
|
|
function validatePacket(packet, result) {
|
|
if (!packet || !/^[0-9a-f]{64}$/.test(packet.packetHash || "") || packet.packetHash !== packetHash(packet)) throw new Error("Packet hash mismatch");
|
|
const expected = buildPacket(result);
|
|
if (canonicalJson(packet) !== canonicalJson(expected)) throw new Error("Packet differs from canonical deterministic packet");
|
|
if (Buffer.byteLength(canonicalJson(packet), "utf8") > PILOT_LIMITS.maxPacketBytes) throw new Error("Packet exceeds byte limit");
|
|
return true;
|
|
}
|
|
|
|
function validateInterpretation(value, packet) {
|
|
assertKeys(value, ["schemaVersion", "kind", "reviewer", "skillId", "bundleHash", "cacheKey", "packetHash", "dimensions", "positives", "shortcomings", "improvements"], "Interpretation");
|
|
if (value.schemaVersion !== SCHEMA_VERSION || value.kind !== "aas-codex-app-interpretation") throw new Error("Interpretation schema mismatch");
|
|
if (!/^[0-9a-f]{64}$/.test(value.packetHash || "") || value.skillId !== packet.skillId || value.bundleHash !== packet.bundleHash || value.cacheKey !== packet.cacheKey || value.packetHash !== packet.packetHash) throw new Error("Interpretation packet binding mismatch");
|
|
if (canonicalJson(value.reviewer) !== canonicalJson(SEMANTIC_REVIEWER)) throw new Error("Interpretation semantic reviewer binding mismatch");
|
|
assertKeys(value.dimensions, ["description", "content"], "Interpretation dimensions");
|
|
for (const kind of ["description", "content"]) {
|
|
const expected = Object.keys(DIMENSIONS[kind]);
|
|
assertKeys(value.dimensions[kind], expected, `Interpretation ${kind}`);
|
|
for (const name of expected) {
|
|
const item = value.dimensions[kind][name];
|
|
assertKeys(item, ["verdict", "note", "evidence_ids"], `Interpretation ${kind}.${name}`);
|
|
if (!["agree", "uncertain", "disagree"].includes(item.verdict)) throw new Error("Interpretation verdict invalid");
|
|
requiredString(item.note, "interpretation note", PILOT_LIMITS.maxNarrativeBytes);
|
|
if (secretLike(item.note)) throw new Error("Interpretation narrative contains a secret-like value");
|
|
if (!Array.isArray(item.evidence_ids) || item.evidence_ids.length < 1 || item.evidence_ids.length > PILOT_LIMITS.maxEvidenceItemsPerDimension) throw new Error("Interpretation evidence ids invalid");
|
|
const allowedEvidence = new Set(packet.deterministic.dimensions[kind][name].evidence_ids);
|
|
for (const id of item.evidence_ids) if (!allowedEvidence.has(id)) throw new Error("Interpretation evidence id outside its dimension");
|
|
}
|
|
}
|
|
for (const field of ["positives", "shortcomings", "improvements"]) {
|
|
if (!Array.isArray(value[field]) || value[field].length < 1 || value[field].length > 8) throw new Error(`Interpretation ${field} invalid`);
|
|
for (const item of value[field]) {
|
|
requiredString(item, `interpretation ${field}`, PILOT_LIMITS.maxNarrativeBytes);
|
|
if (secretLike(item)) throw new Error("Interpretation narrative contains a secret-like value");
|
|
}
|
|
}
|
|
if (Buffer.byteLength(canonicalJson(value), "utf8") > PILOT_LIMITS.maxInterpretationBytes) throw new Error("Interpretation exceeds byte limit");
|
|
return true;
|
|
}
|
|
|
|
function validateProposal(value) {
|
|
assertKeys(value, ["schemaVersion", "kind", "skillId", "targetPath", "inputSha256", "candidateSha256", "patchSha256", "versions", "packetHash", "interpretationHash", "reviewer", "applyCapability", "isolatedRootDeleted", "patchCheck", "validation", "analysis"], "Proposal");
|
|
if (value.schemaVersion !== SCHEMA_VERSION || value.kind !== "aas-local-skill-proposal" || canonicalJson(value.reviewer) !== canonicalJson(SEMANTIC_REVIEWER)) throw new Error("Proposal schema mismatch");
|
|
if (![value.inputSha256, value.candidateSha256, value.patchSha256, value.packetHash, value.interpretationHash].every((item) => /^[0-9a-f]{64}$/.test(item))) throw new Error("Proposal binding hash invalid");
|
|
if (value.applyCapability !== false || value.isolatedRootDeleted !== true || value.patchCheck !== "passed") throw new Error("Proposal safety attestation invalid");
|
|
if (canonicalJson(value.versions) !== canonicalJson(expectedVersions())) throw new Error("Proposal version mismatch");
|
|
return true;
|
|
}
|
|
|
|
function validateProposalCompletion(value) {
|
|
assertKeys(value, ["schemaVersion", "kind", "skillId", "patchSha256", "reportSha256"], "Proposal completion");
|
|
if (value.schemaVersion !== SCHEMA_VERSION || value.kind !== "aas-local-skill-proposal-complete") throw new Error("Proposal completion schema mismatch");
|
|
requiredString(value.skillId, "Proposal completion skill id", 1024);
|
|
if (![value.patchSha256, value.reportSha256].every((item) => /^[0-9a-f]{64}$/.test(item))) throw new Error("Proposal completion hash invalid");
|
|
return true;
|
|
}
|
|
|
|
module.exports = { assertKeys, buildResult, expectedVersions, requiredString, sanitizeJudgments, validateInterpretation, validatePacket, validateProposal, validateProposalCompletion, validateResult, validateRuntimeResult };
|