feat(orchestrator): observe Page Data validation
This commit is contained in:
@@ -16,8 +16,18 @@ export const DEFAULT_ORCHESTRATED_WORKFLOWS = Object.freeze([
|
||||
'code-run-v1',
|
||||
]);
|
||||
|
||||
export const PAGE_DATA_VALIDATION_OBSERVATION_VERSION =
|
||||
'page-data-validation-observation-v1';
|
||||
|
||||
const ENGINE_ID_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
|
||||
const WORKFLOW_NAME_PATTERN = /^[a-z][a-z0-9_-]{0,127}$/;
|
||||
const VALIDATION_CHECK_ID_PATTERN = /^[a-z][a-z0-9_-]{0,127}$/;
|
||||
const VALIDATION_CHECK_STATUSES = new Set(['passed', 'failed', 'skipped']);
|
||||
const REQUIRED_PAGE_DATA_CHECKS = Object.freeze([
|
||||
'agent_run_completion',
|
||||
'page_data_binding',
|
||||
'page_data_storage_policy',
|
||||
]);
|
||||
|
||||
function normalizeIdentifier(value, pattern, fallback) {
|
||||
const normalized = String(value ?? '').trim().toLowerCase();
|
||||
@@ -98,6 +108,82 @@ export function buildRunEvent({
|
||||
};
|
||||
}
|
||||
|
||||
function validationContractError(message) {
|
||||
const error = new Error(message);
|
||||
error.code = 'WORKFLOW_VALIDATION_INVALID';
|
||||
error.status = 422;
|
||||
return error;
|
||||
}
|
||||
|
||||
function normalizeValidationCount(value) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed) || parsed < 0) return 0;
|
||||
return Math.min(1_000_000, Math.floor(parsed));
|
||||
}
|
||||
|
||||
export function normalizePageDataValidationObservation(input = {}) {
|
||||
const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
|
||||
const idempotencyKey = String(source.idempotencyKey ?? '').trim().slice(0, 200);
|
||||
if (!idempotencyKey) {
|
||||
throw validationContractError('Page Data validation observation requires idempotencyKey');
|
||||
}
|
||||
if (!Array.isArray(source.checks) || source.checks.length === 0) {
|
||||
throw validationContractError('Page Data validation observation requires checks');
|
||||
}
|
||||
const checks = source.checks.slice(0, 32).map((item) => {
|
||||
const check = item && typeof item === 'object' && !Array.isArray(item) ? item : {};
|
||||
const id = normalizeIdentifier(check.id, VALIDATION_CHECK_ID_PATTERN, '');
|
||||
const status = String(check.status ?? '').trim().toLowerCase();
|
||||
if (!id || !VALIDATION_CHECK_STATUSES.has(status)) {
|
||||
throw validationContractError('Page Data validation observation contains an invalid check');
|
||||
}
|
||||
return {
|
||||
id,
|
||||
status,
|
||||
codes: normalizeStringList(check.codes, {
|
||||
limit: 32,
|
||||
itemLimit: 128,
|
||||
normalize: (value) => String(value ?? '').trim().toUpperCase(),
|
||||
}),
|
||||
};
|
||||
});
|
||||
const byId = new Map(checks.map((check) => [check.id, check]));
|
||||
const failed = checks.some((check) => check.status === 'failed');
|
||||
const required = source.required === true;
|
||||
const requiredChecksPassed = REQUIRED_PAGE_DATA_CHECKS.every(
|
||||
(id) => byId.get(id)?.status === 'passed',
|
||||
);
|
||||
const verdict = failed
|
||||
? 'failed'
|
||||
: required && !requiredChecksPassed
|
||||
? 'inconclusive'
|
||||
: checks.some((check) => check.status === 'passed')
|
||||
? 'passed'
|
||||
: 'inconclusive';
|
||||
const taskType = normalizeWorkflowName(source.taskType, '');
|
||||
const observedAt = Number(source.observedAt);
|
||||
return {
|
||||
version: PAGE_DATA_VALIDATION_OBSERVATION_VERSION,
|
||||
kind: 'page-data-delivery',
|
||||
idempotencyKey,
|
||||
taskType: taskType || null,
|
||||
required,
|
||||
verdict,
|
||||
checks,
|
||||
metrics: {
|
||||
pageCount: normalizeValidationCount(source.metrics?.pageCount),
|
||||
publicationCount: normalizeValidationCount(source.metrics?.publicationCount),
|
||||
},
|
||||
source: normalizeIdentifier(
|
||||
source.source,
|
||||
VALIDATION_CHECK_ID_PATTERN,
|
||||
'portal-agent-run',
|
||||
),
|
||||
dataPolicy: 'control-plane-only-v1',
|
||||
observedAt: Number.isFinite(observedAt) && observedAt > 0 ? observedAt : Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
export function stableRolloutBucket(value) {
|
||||
const hash = crypto.createHash('sha256').update(String(value ?? '')).digest();
|
||||
return hash.readUInt32BE(0) % 100;
|
||||
|
||||
Reference in New Issue
Block a user