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)) }