NODEDC_PLATFORM/services/ontology-core/src/validate.mjs

364 lines
20 KiB
JavaScript

import fs from 'node:fs/promises'
import path from 'node:path'
import { loadCatalog, serviceRoot } from './catalog.mjs'
const ID_RE = /^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9_]*)+$/
const RULE_ID_RE = /^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$/
const ROLE_ID_RE = /^[a-z][a-z0-9_]*$/
const ALLOWED_RELATION_STATUSES = new Set([
'source-confirmed',
'source-evidenced',
'owner-confirmed',
'product-required',
'future-concept',
'tech-debt-noncanonical',
'pending',
])
function assert(condition, message, errors) {
if (!condition) errors.push(message)
}
function assertUnique(ids, label, errors) {
const seen = new Set()
for (const id of ids) {
assert(!seen.has(id), `${label} duplicate id: ${id}`, errors)
seen.add(id)
}
}
function assertEntityRefs(refs, entityById, label, errors) {
for (const ref of refs || []) {
assert(entityById.has(ref), `${label} references unknown entity: ${ref}`, errors)
}
}
function assertRelationRef(ref, relationById, label, errors) {
assert(relationById.has(ref), `${label} references unknown relation: ${ref}`, errors)
}
function assertContextRefs(refs, contextById, label, errors) {
for (const ref of refs || []) {
assert(contextById.has(ref), `${label} references unknown context: ${ref}`, errors)
}
}
function assertRefsObject(refs, label, errors) {
assert(refs && typeof refs === 'object' && !Array.isArray(refs), `${label} refs must be object`, errors)
for (const [key, value] of Object.entries(refs || {})) {
assert(/^[a-z][a-z0-9_]*$/i.test(key), `${label} refs has invalid key: ${key}`, errors)
assert(value !== null && value !== undefined && String(value).trim() !== '', `${label} refs.${key} is empty`, errors)
}
}
async function pathExists(relativePath) {
try {
await fs.access(path.join(serviceRoot, relativePath))
return true
} catch {
return false
}
}
export async function validateCatalog() {
const catalog = await loadCatalog()
const errors = []
const warnings = []
const {
entities,
relations,
aliases,
guardrails,
evidence,
resolverRules,
assistantAccessPolicy,
assistantActions,
assistantRiskPolicy,
contextBindings,
entityById,
relationById,
contextById,
bindingTypeById,
} = catalog
const statusVocabulary = new Set(entities.statusVocabulary || [])
assertUnique(entities.entities.map((entity) => entity.id), 'entity', errors)
for (const entity of entities.entities) {
assert(ID_RE.test(entity.id), `entity id has invalid format: ${entity.id}`, errors)
assert(entity.name, `entity ${entity.id} missing name`, errors)
assert(entity.surface, `entity ${entity.id} missing surface`, errors)
assert(Array.isArray(entity.status) && entity.status.length > 0, `entity ${entity.id} missing status`, errors)
for (const status of entity.status || []) {
assert(statusVocabulary.has(status), `entity ${entity.id} uses unknown status: ${status}`, errors)
}
}
assertUnique(relations.relations.map((relation) => relation.id), 'relation', errors)
for (const relation of relations.relations) {
assert(RULE_ID_RE.test(relation.id), `relation id has invalid format: ${relation.id}`, errors)
assert(ALLOWED_RELATION_STATUSES.has(relation.status), `relation ${relation.id} uses unknown status: ${relation.status}`, errors)
assertEntityRefs(relation.from, entityById, `relation ${relation.id}.from`, errors)
assertEntityRefs(relation.to, entityById, `relation ${relation.id}.to`, errors)
}
assertUnique(aliases.aliases.map((alias) => alias.alias.toLowerCase()), 'alias', errors)
for (const alias of aliases.aliases) {
assert(alias.alias, 'alias missing alias text', errors)
assert(entityById.has(alias.canonicalId), `alias ${alias.alias} points to unknown entity: ${alias.canonicalId}`, errors)
}
assertUnique(guardrails.rules.map((rule) => rule.id), 'guardrail', errors)
for (const rule of guardrails.rules) {
assert(RULE_ID_RE.test(rule.id), `guardrail id has invalid format: ${rule.id}`, errors)
assert(['error', 'warning'].includes(rule.severity), `guardrail ${rule.id} invalid severity`, errors)
assertEntityRefs(rule.entityIds, entityById, `guardrail ${rule.id}`, errors)
}
for (const pair of guardrails.blockedConflations || []) {
assert(Array.isArray(pair) && pair.length === 2, `blocked conflation must be a pair: ${JSON.stringify(pair)}`, errors)
assertEntityRefs(pair, entityById, 'blocked conflation', errors)
}
for (const ledger of evidence.ledgers || []) {
assert(await pathExists(ledger.path), `evidence ledger path missing: ${ledger.path}`, errors)
assertEntityRefs(ledger.entityIds, entityById, `ledger ${ledger.id}`, errors)
}
for (const baselineDoc of evidence.baselineDocs || []) {
assert(await pathExists(baselineDoc), `baseline doc path missing: ${baselineDoc}`, errors)
}
assertUnique(resolverRules.rules.map((rule) => rule.id), 'resolver rule', errors)
for (const rule of resolverRules.rules) {
assert(RULE_ID_RE.test(rule.id), `resolver rule id has invalid format: ${rule.id}`, errors)
assert(rule.fromSurface, `resolver rule ${rule.id} missing fromSurface`, errors)
assert(rule.outputSurface, `resolver rule ${rule.id} missing outputSurface`, errors)
assertRelationRef(rule.relationId, relationById, `resolver rule ${rule.id}`, errors)
assertEntityRefs(rule.inputEntityIds, entityById, `resolver rule ${rule.id}.inputEntityIds`, errors)
assertEntityRefs(rule.outputEntityIds, entityById, `resolver rule ${rule.id}.outputEntityIds`, errors)
if (rule.status !== 'product-required') {
warnings.push(`resolver rule ${rule.id} has non-product-required status: ${rule.status}`)
}
}
const assistantRoleIds = new Set((assistantAccessPolicy.roleVocabulary || []).map((role) => role.id))
const assistantCapabilityIds = new Set((assistantAccessPolicy.capabilities || []).map((capability) => capability.id))
const assistantOperationIds = new Set((assistantAccessPolicy.operations || []).map((operation) => operation.id))
const assistantHardDenyIds = new Set((assistantAccessPolicy.hardDenies || []).map((deny) => deny.id))
const assistantRiskLevelIds = new Set((assistantRiskPolicy.riskLevels || []).map((riskLevel) => riskLevel.id))
const assistantConfirmationModeIds = new Set((assistantRiskPolicy.confirmationModes || []).map((mode) => mode.id))
const assistantSelfActionPolicyIds = new Set((assistantRiskPolicy.selfActionPolicies || []).map((policy) => policy.id))
const assistantActionHardDenyIds = new Set((assistantRiskPolicy.hardDenies || []).map((deny) => deny.id))
const assistantAppIds = new Set((assistantActions.apps || []).map((app) => app.id))
const assistantAdapterStatuses = new Set(assistantActions.adapterStatuses || [])
const assistantActionIds = new Set((assistantActions.actions || []).map((action) => action.id))
assertUnique([...assistantRoleIds], 'assistant role', errors)
for (const role of assistantAccessPolicy.roleVocabulary || []) {
assert(ROLE_ID_RE.test(role.id), `assistant role id has invalid format: ${role.id}`, errors)
assert(role.name, `assistant role ${role.id} missing name`, errors)
assert(role.summary, `assistant role ${role.id} missing summary`, errors)
}
assertUnique((assistantAccessPolicy.roleAliases || []).map((alias) => alias.alias.toLowerCase()), 'assistant role alias', errors)
for (const alias of assistantAccessPolicy.roleAliases || []) {
assert(alias.alias, 'assistant role alias missing alias text', errors)
assert(assistantRoleIds.has(alias.roleId), `assistant role alias ${alias.alias} references unknown role: ${alias.roleId}`, errors)
}
for (const [key, roleId] of Object.entries(assistantAccessPolicy.defaultResolution || {})) {
if (key.endsWith('Role')) {
assert(assistantRoleIds.has(roleId), `assistant defaultResolution.${key} references unknown role: ${roleId}`, errors)
}
}
for (const sourceAuthority of assistantAccessPolicy.sourceAuthorities || []) {
assert(RULE_ID_RE.test(sourceAuthority.id), `assistant source authority id has invalid format: ${sourceAuthority.id}`, errors)
assertEntityRefs(sourceAuthority.entityIds, entityById, `assistant source authority ${sourceAuthority.id}`, errors)
}
assertUnique([...assistantCapabilityIds], 'assistant capability', errors)
for (const capability of assistantAccessPolicy.capabilities || []) {
assert(RULE_ID_RE.test(capability.id), `assistant capability id has invalid format: ${capability.id}`, errors)
assert(entityById.has(capability.entityId), `assistant capability ${capability.id} references unknown entity: ${capability.entityId}`, errors)
assert(capability.summary, `assistant capability ${capability.id} missing summary`, errors)
}
assertUnique((assistantAccessPolicy.roleMatrix || []).map((matrix) => matrix.roleId), 'assistant role matrix', errors)
for (const matrix of assistantAccessPolicy.roleMatrix || []) {
assert(assistantRoleIds.has(matrix.roleId), `assistant matrix references unknown role: ${matrix.roleId}`, errors)
for (const capabilityId of matrix.allowedCapabilities || []) {
assert(assistantCapabilityIds.has(capabilityId), `assistant matrix ${matrix.roleId} allows unknown capability: ${capabilityId}`, errors)
}
for (const capabilityId of matrix.deniedCapabilities || []) {
if (capabilityId === '*') continue
assert(assistantCapabilityIds.has(capabilityId), `assistant matrix ${matrix.roleId} denies unknown capability: ${capabilityId}`, errors)
}
for (const capabilityId of matrix.deniedWhenNoAdminScope || []) {
assert(assistantCapabilityIds.has(capabilityId), `assistant matrix ${matrix.roleId} no-scope denies unknown capability: ${capabilityId}`, errors)
}
for (const conditional of matrix.conditionalCapabilities || []) {
assert(assistantCapabilityIds.has(conditional.capabilityId), `assistant matrix ${matrix.roleId} conditional unknown capability: ${conditional.capabilityId}`, errors)
assert(conditional.requires, `assistant matrix ${matrix.roleId} conditional ${conditional.capabilityId} missing requires`, errors)
}
}
assertUnique([...assistantOperationIds], 'assistant operation', errors)
for (const operation of assistantAccessPolicy.operations || []) {
assert(RULE_ID_RE.test(operation.id), `assistant operation id has invalid format: ${operation.id}`, errors)
assert(assistantCapabilityIds.has(operation.capabilityId), `assistant operation ${operation.id} references unknown capability: ${operation.capabilityId}`, errors)
for (const denyId of operation.hardDenies || []) {
assert(assistantHardDenyIds.has(denyId), `assistant operation ${operation.id} references unknown hard deny: ${denyId}`, errors)
}
assert(operation.summary, `assistant operation ${operation.id} missing summary`, errors)
}
assertUnique((assistantAccessPolicy.hardDenies || []).map((deny) => deny.id), 'assistant hard deny', errors)
for (const deny of assistantAccessPolicy.hardDenies || []) {
assert(RULE_ID_RE.test(deny.id), `assistant hard deny id has invalid format: ${deny.id}`, errors)
assertEntityRefs(deny.entityIds, entityById, `assistant hard deny ${deny.id}`, errors)
}
assertUnique([...assistantRiskLevelIds], 'assistant risk level', errors)
for (const riskLevel of assistantRiskPolicy.riskLevels || []) {
assert(ROLE_ID_RE.test(riskLevel.id), `assistant risk level id has invalid format: ${riskLevel.id}`, errors)
assert(riskLevel.summary, `assistant risk level ${riskLevel.id} missing summary`, errors)
}
assertUnique([...assistantConfirmationModeIds], 'assistant confirmation mode', errors)
for (const mode of assistantRiskPolicy.confirmationModes || []) {
assert(ROLE_ID_RE.test(mode.id), `assistant confirmation mode id has invalid format: ${mode.id}`, errors)
assert(mode.summary, `assistant confirmation mode ${mode.id} missing summary`, errors)
}
assertUnique([...assistantSelfActionPolicyIds], 'assistant self action policy', errors)
for (const policy of assistantRiskPolicy.selfActionPolicies || []) {
assert(ROLE_ID_RE.test(policy.id), `assistant self action policy id has invalid format: ${policy.id}`, errors)
assert(policy.summary, `assistant self action policy ${policy.id} missing summary`, errors)
}
assertUnique([...assistantActionHardDenyIds], 'assistant action hard deny', errors)
for (const deny of assistantRiskPolicy.hardDenies || []) {
assert(RULE_ID_RE.test(deny.id), `assistant action hard deny id has invalid format: ${deny.id}`, errors)
for (const actionId of deny.safeAlternatives || []) {
assert(assistantActionIds.has(actionId), `assistant action hard deny ${deny.id} references unknown safe alternative: ${actionId}`, errors)
}
}
assertUnique((assistantRiskPolicy.globalRules || []).map((rule) => rule.id), 'assistant risk global rule', errors)
for (const rule of assistantRiskPolicy.globalRules || []) {
assert(RULE_ID_RE.test(rule.id), `assistant risk global rule id has invalid format: ${rule.id}`, errors)
assert(['error', 'warning'].includes(rule.severity), `assistant risk global rule ${rule.id} invalid severity`, errors)
assert(rule.summary, `assistant risk global rule ${rule.id} missing summary`, errors)
}
assertUnique([...assistantAppIds], 'assistant app', errors)
for (const app of assistantActions.apps || []) {
assert(ROLE_ID_RE.test(app.id), `assistant app id has invalid format: ${app.id}`, errors)
assert(app.name, `assistant app ${app.id} missing name`, errors)
assert(app.adapterId, `assistant app ${app.id} missing adapterId`, errors)
assertEntityRefs(app.authorityEntityIds, entityById, `assistant app ${app.id}.authorityEntityIds`, errors)
}
assertUnique([...assistantAdapterStatuses], 'assistant adapter status', errors)
for (const status of assistantActions.adapterStatuses || []) {
assert(ROLE_ID_RE.test(status), `assistant adapter status has invalid format: ${status}`, errors)
}
assertUnique([...assistantActionIds], 'assistant action', errors)
for (const action of assistantActions.actions || []) {
assert(RULE_ID_RE.test(action.id), `assistant action id has invalid format: ${action.id}`, errors)
assert(assistantAppIds.has(action.app), `assistant action ${action.id} references unknown app: ${action.app}`, errors)
assert(action.domain, `assistant action ${action.id} missing domain`, errors)
assert(Array.isArray(action.intentAliases), `assistant action ${action.id} intentAliases must be array`, errors)
assertEntityRefs(action.entityIds, entityById, `assistant action ${action.id}.entityIds`, errors)
for (const capabilityId of action.capabilityIds || []) {
assert(assistantCapabilityIds.has(capabilityId), `assistant action ${action.id} references unknown capability: ${capabilityId}`, errors)
}
assert(assistantRiskLevelIds.has(action.riskLevel), `assistant action ${action.id} references unknown riskLevel: ${action.riskLevel}`, errors)
assert(assistantConfirmationModeIds.has(action.confirmationMode), `assistant action ${action.id} references unknown confirmationMode: ${action.confirmationMode}`, errors)
assert(assistantSelfActionPolicyIds.has(action.selfActionPolicy), `assistant action ${action.id} references unknown selfActionPolicy: ${action.selfActionPolicy}`, errors)
assert(action.adapterId, `assistant action ${action.id} missing adapterId`, errors)
assert(assistantAdapterStatuses.has(action.adapterStatus), `assistant action ${action.id} references unknown adapterStatus: ${action.adapterStatus}`, errors)
for (const denyId of action.hardDenyIds || []) {
assert(assistantActionHardDenyIds.has(denyId), `assistant action ${action.id} references unknown hard deny: ${denyId}`, errors)
}
for (const actionId of action.safeAlternativeActionIds || []) {
assert(assistantActionIds.has(actionId), `assistant action ${action.id} references unknown safe alternative action: ${actionId}`, errors)
}
if (action.riskLevel === 'destructive') {
assert(action.confirmationMode === 'forbidden', `assistant action ${action.id} destructive action must use confirmationMode=forbidden`, errors)
assert(action.adapterStatus === 'forbidden', `assistant action ${action.id} destructive action must use adapterStatus=forbidden`, errors)
}
if (action.confirmationMode === 'forbidden') {
assert(action.refusal, `assistant action ${action.id} forbidden action missing refusal`, errors)
}
if (action.riskLevel === 'privileged') {
assert(Array.isArray(action.requiredScopes) && action.requiredScopes.length > 0, `assistant action ${action.id} privileged action missing requiredScopes`, errors)
}
}
const contextStatuses = new Set(contextBindings.contextStatuses || [])
const bindingStatuses = new Set(contextBindings.bindingStatuses || [])
assertUnique(contextBindings.contexts.map((context) => context.id), 'context', errors)
for (const context of contextBindings.contexts) {
assert(RULE_ID_RE.test(context.id), `context id has invalid format: ${context.id}`, errors)
assert(entityById.has(context.entityId), `context ${context.id} references unknown entity: ${context.entityId}`, errors)
assert(contextStatuses.has(context.status), `context ${context.id} has invalid status: ${context.status}`, errors)
assert(context.surface, `context ${context.id} missing surface`, errors)
assert(context.sourceSystem, `context ${context.id} missing sourceSystem`, errors)
assertRefsObject(context.refs, `context ${context.id}`, errors)
if (context.parentContextId) {
assertContextRefs([context.parentContextId], contextById, `context ${context.id}`, errors)
}
}
assertUnique(contextBindings.bindingTypes.map((bindingType) => bindingType.id), 'binding type', errors)
for (const bindingType of contextBindings.bindingTypes) {
assert(RULE_ID_RE.test(bindingType.id), `binding type id has invalid format: ${bindingType.id}`, errors)
assertRelationRef(bindingType.relationId, relationById, `binding type ${bindingType.id}`, errors)
assertEntityRefs(bindingType.fromEntityIds, entityById, `binding type ${bindingType.id}.fromEntityIds`, errors)
assertEntityRefs(bindingType.toEntityIds, entityById, `binding type ${bindingType.id}.toEntityIds`, errors)
}
assertUnique(contextBindings.bindings.map((binding) => binding.id), 'binding', errors)
for (const binding of contextBindings.bindings) {
assert(RULE_ID_RE.test(binding.id), `binding id has invalid format: ${binding.id}`, errors)
assert(bindingStatuses.has(binding.status), `binding ${binding.id} has invalid status: ${binding.status}`, errors)
assert(bindingTypeById.has(binding.typeId), `binding ${binding.id} references unknown binding type: ${binding.typeId}`, errors)
assertContextRefs([binding.fromContextId, binding.toContextId], contextById, `binding ${binding.id}`, errors)
}
return {
ok: errors.length === 0,
errors,
warnings,
counts: {
entities: entities.entities.length,
relations: relations.relations.length,
aliases: aliases.aliases.length,
guardrails: guardrails.rules.length,
evidenceLedgers: evidence.ledgers.length,
resolverRules: resolverRules.rules.length,
assistantRoles: assistantAccessPolicy.roleVocabulary.length,
assistantCapabilities: assistantAccessPolicy.capabilities.length,
assistantOperations: assistantAccessPolicy.operations.length,
assistantActions: assistantActions.actions.length,
assistantApps: assistantActions.apps.length,
assistantRiskRules: assistantRiskPolicy.globalRules.length,
contexts: contextBindings.contexts.length,
bindingTypes: contextBindings.bindingTypes.length,
bindings: contextBindings.bindings.length,
},
}
}
if (import.meta.url === `file://${process.argv[1]}`) {
const result = await validateCatalog()
if (!result.ok) {
console.error(JSON.stringify(result, null, 2))
process.exit(1)
}
console.log(JSON.stringify(result, null, 2))
}