/** * Builtin image `tfp-builtin/json-schema-check:1` — a structural check: the * submitted artifact (role `document`) must parse as JSON and satisfy the * JSON Schema bound to the Contract's `document_schema` reference slot. * * Contract semantics: verified when the document parses and validates; * refuted when it is well-formed JSON that violates the schema (a wrong * shape is a statement about the work); inconclusive when it is not JSON * or not UTF-8 at all (unreadable, not wrong). The check establishes * structure only: nothing about whether the content is true. */ import type { SandboxFs } from '../sandbox.js'; import { unsupportedKeywords, validateSchema, violationLines, type JsonSchema } from '../../json-schema.js'; import type { RunManifest } from '../../contracts.js'; export const JSON_SCHEMA_IMAGE_REF = 'tfp-builtin/json-schema-check:1'; const MAX_VIOLATIONS_REPORTED = 50; export async function jsonSchemaCheck(fs: SandboxFs): Promise { const manifest = JSON.parse(fs.readText('/input/manifest.json')) as RunManifest; const finish = (result: 'verified' | 'refuted' | 'inconclusive', reason: string, violations: string[] = []) => { fs.writeFile('/output/evidence/summary.json', JSON.stringify({ valid: result === 'verified', violations_count: violations.length, violations: violations.slice(0, MAX_VIOLATIONS_REPORTED) })); fs.writeFile('/output/result.json', JSON.stringify({ result, reason, findings: violations.slice(0, MAX_VIOLATIONS_REPORTED) })); fs.log(`${result}: ${reason}`); return 0; }; const schemaPath = manifest.reference.find((r) => r.key === 'document_schema')?.path; if (!schemaPath) return finish('inconclusive', 'no bound document_schema'); let schema: JsonSchema; try { schema = JSON.parse(fs.readText(schemaPath)) as JsonSchema; } catch { return finish('inconclusive', 'the bound document_schema is not valid JSON'); } const unsupported = unsupportedKeywords(schema); if (unsupported.length) return finish('inconclusive', `the bound document_schema uses keywords this checker does not implement: ${unsupported.slice(0, 5).join(', ')}`); const docPath = manifest.artifacts.find((a) => a.role === 'document')?.path; if (!docPath) return finish('inconclusive', 'no document artifact'); let text: string; try { text = fs.readText(docPath); } catch { return finish('inconclusive', 'document is not UTF-8 text'); } let doc: unknown; try { doc = JSON.parse(text); } catch (e) { return finish('inconclusive', `document is not valid JSON: ${(e as Error).message.slice(0, 120)}`); } fs.log(`document: ${text.length} characters of JSON`); const violations = violationLines(validateSchema(schema, doc)); if (violations.length) return finish('refuted', `document violates the schema at ${violations.length} place(s); first: ${violations[0]}`, violations); return finish('verified', 'document parses and satisfies the bound schema'); }