@openplanr/protocol 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/lib/dashboard/closed-json-contract.mjs +141 -0
- package/lib/dashboard/generated/operate-experience-surface-schema-data.mjs +67 -0
- package/lib/dashboard/generated/operate-review-schema-data.mjs +35 -0
- package/lib/dashboard/generated/operate-schema-token-codec.mjs +127 -0
- package/lib/dashboard/operate-experience-audit-display-contract.d.mts +196 -0
- package/lib/dashboard/operate-experience-audit-display-contract.mjs +700 -0
- package/lib/dashboard/operate-experience-display-contract.d.mts +572 -0
- package/lib/dashboard/operate-experience-display-contract.mjs +1007 -0
- package/lib/dashboard/operate-experience-surface-contract.d.mts +157 -0
- package/lib/dashboard/operate-experience-surface-contract.mjs +91 -0
- package/lib/dashboard/operate-review-contract.d.mts +34 -0
- package/lib/dashboard/operate-review-contract.mjs +175 -0
- package/lib/dashboard/operate-review-display-workspace-contract.d.mts +54 -0
- package/lib/dashboard/operate-review-display-workspace-contract.mjs +258 -0
- package/lib/dashboard/operate-review-payload-safety.d.mts +7 -0
- package/lib/dashboard/operate-review-payload-safety.mjs +42 -0
- package/lib/dashboard/operate-review-workspace-projection-v2.d.mts +218 -0
- package/lib/protocol/index.d.ts +2638 -0
- package/package.json +115 -0
- package/registries/commands.json +1952 -0
- package/registries/diagram-grammars.json +2407 -0
- package/registries/diagram-semantic-patterns.json +111 -0
- package/registries/output-paths.json +40 -0
- package/registries/outputs.json +864 -0
- package/registries/roles.json +661 -0
- package/registries/rules.json +261 -0
- package/registries/skill-host-profiles.json +247 -0
- package/registries/skill-modules.json +1112 -0
- package/registries/skill-routing.json +389 -0
- package/registries/skills.json +1545 -0
- package/registries/task-kinds.json +220 -0
- package/registry/adapters.json +110 -0
- package/registry/artifact-theme.json +60 -0
- package/registry/evaluation-graders.json +303 -0
- package/registry/evaluation-host-profiles.json +131 -0
- package/registry/frozen-commands.json +15 -0
- package/registry/landing-operations.json +20 -0
- package/registry/landing-workflows.json +70 -0
- package/registry/live-evidence-providers.json +1 -0
- package/registry/operate-v2-contracts.json +5965 -0
- package/registry/professional-skills.json +115 -0
- package/registry/roles.json +14 -0
- package/registry/ship-review-specialists.json +42 -0
- package/schemas/v1.0.0/design-approved.schema.json +35 -0
- package/schemas/v1.0.0/design-feedback.schema.json +169 -0
- package/schemas/v1.0.0/design-manifest.schema.json +90 -0
- package/schemas/v1.0.0/design-session.schema.json +73 -0
- package/schemas/v1.0.0/graph.schema.json +100 -0
- package/schemas/v1.0.0/pipeline-shipped.schema.json +154 -0
- package/schemas/v1.0.0/run-manifest.schema.json +81 -0
- package/schemas/v1.0.0/spec.schema.json +120 -0
- package/schemas/v1.0.0/stack.schema.json +184 -0
- package/schemas/v1.0.0/story.schema.json +95 -0
- package/schemas/v1.0.0/task.schema.json +171 -0
- package/schemas/v1.0.0/taste-profile.schema.json +96 -0
- package/schemas/v1.1.0/adapter-registry.schema.json +72 -0
- package/schemas/v1.1.0/artifact-envelope.schema.json +142 -0
- package/schemas/v1.1.0/artifact-paste.schema.json +49 -0
- package/schemas/v1.1.0/artifact-review.schema.json +97 -0
- package/schemas/v1.1.0/artifact-room-descriptor.schema.json +39 -0
- package/schemas/v1.1.0/artifact-room-event.schema.json +184 -0
- package/schemas/v1.1.0/artifact-room-signed-event.schema.json +58 -0
- package/schemas/v1.1.0/artifact-theme.schema.json +139 -0
- package/schemas/v1.1.0/browser-qa-gate.schema.json +21 -0
- package/schemas/v1.1.0/browser-qa-result.schema.json +24 -0
- package/schemas/v1.1.0/browser-qa-session.schema.json +12 -0
- package/schemas/v1.1.0/ecosystem-manifest.schema.json +46 -0
- package/schemas/v1.1.0/investigation-diagnosis.schema.json +12 -0
- package/schemas/v1.1.0/investigation-event.schema.json +15 -0
- package/schemas/v1.1.0/investigation-experiment.schema.json +17 -0
- package/schemas/v1.1.0/investigation-fix.schema.json +11 -0
- package/schemas/v1.1.0/investigation-hypothesis.schema.json +6 -0
- package/schemas/v1.1.0/investigation-observation.schema.json +10 -0
- package/schemas/v1.1.0/investigation-receipt.schema.json +7 -0
- package/schemas/v1.1.0/investigation-record.schema.json +7 -0
- package/schemas/v1.1.0/investigation-request.schema.json +34 -0
- package/schemas/v1.1.0/planning-review-event.schema.json +64 -0
- package/schemas/v1.1.0/planning-review-receipt.schema.json +19 -0
- package/schemas/v1.1.0/planning-review-record.schema.json +176 -0
- package/schemas/v1.1.0/planning-review-state.schema.json +19 -0
- package/schemas/v1.1.0/professional-skills.schema.json +105 -0
- package/schemas/v1.1.0/professional-specification.schema.json +111 -0
- package/schemas/v1.1.0/provenance-event.schema.json +82 -0
- package/schemas/v1.1.0/role-registry.schema.json +28 -0
- package/schemas/v1.1.0/runtime-lock.schema.json +39 -0
- package/schemas/v1.1.0/ship-closure.schema.json +353 -0
- package/schemas/v1.1.0/ship-review-specialist-registry.schema.json +29 -0
- package/schemas/v1.1.0/ship-risk-classification.schema.json +48 -0
- package/schemas/v1.1.0/specialist-review-result.schema.json +19 -0
- package/schemas/v1.10.0/design-review-bundle.schema.json +584 -0
- package/schemas/v1.10.0/design-review-context.schema.json +191 -0
- package/schemas/v1.10.0/design-review-handoff.schema.json +407 -0
- package/schemas/v1.10.0/design-review-metadata-payload.schema.json +106 -0
- package/schemas/v1.11.0/design-review-metadata-payload.schema.json +107 -0
- package/schemas/v1.12.0/enterprise-access-context.schema.json +217 -0
- package/schemas/v1.12.0/enterprise-agent-handoff.schema.json +508 -0
- package/schemas/v1.12.0/enterprise-artifact-revision.schema.json +87 -0
- package/schemas/v1.12.0/enterprise-change-proposal.schema.json +359 -0
- package/schemas/v1.12.0/enterprise-evidence-reference.schema.json +147 -0
- package/schemas/v1.12.0/enterprise-review-thread.schema.json +278 -0
- package/schemas/v1.12.0/enterprise-sync-state.schema.json +169 -0
- package/schemas/v1.2.0/adapter-registry.schema.json +83 -0
- package/schemas/v1.2.0/dashboard-bootstrap.schema.json +219 -0
- package/schemas/v1.2.0/ecosystem-release-operation.schema.json +90 -0
- package/schemas/v1.2.0/ecosystem-saga.schema.json +54 -0
- package/schemas/v1.2.0/evidence-diagnostic.schema.json +77 -0
- package/schemas/v1.2.0/guided-answer-envelope.schema.json +73 -0
- package/schemas/v1.2.0/guided-confirmation.schema.json +128 -0
- package/schemas/v1.2.0/guided-question.schema.json +242 -0
- package/schemas/v1.2.0/guided-questionnaire.schema.json +278 -0
- package/schemas/v1.2.0/guided-session.schema.json +141 -0
- package/schemas/v1.2.0/landing-confirmation.schema.json +17 -0
- package/schemas/v1.2.0/landing-event.schema.json +12 -0
- package/schemas/v1.2.0/landing-operation-registry.schema.json +16 -0
- package/schemas/v1.2.0/landing-phase-receipt.schema.json +19 -0
- package/schemas/v1.2.0/landing-plan.schema.json +36 -0
- package/schemas/v1.2.0/landing-receipt.schema.json +12 -0
- package/schemas/v1.2.0/landing-workflow-catalog.schema.json +143 -0
- package/schemas/v1.2.0/operate-action-display-workspace.schema.json +119 -0
- package/schemas/v1.2.0/operate-cycle-display-workspace.schema.json +109 -0
- package/schemas/v1.2.0/operate-executive-board-display-surface.schema.json +72 -0
- package/schemas/v1.2.0/operate-experience-audit-display-surface.schema.json +328 -0
- package/schemas/v1.2.0/operate-experience-display-surface.schema.json +46 -0
- package/schemas/v1.2.0/operate-experience-surface.schema.json +590 -0
- package/schemas/v1.2.0/operate-recovery-display-surface.schema.json +85 -0
- package/schemas/v1.2.0/structured-action.schema.json +91 -0
- package/schemas/v1.3.0/adapter-registry.schema.json +84 -0
- package/schemas/v1.3.0/ecosystem-manifest.schema.json +120 -0
- package/schemas/v1.3.0/release-compatibility-claim.schema.json +46 -0
- package/schemas/v1.3.0/release-ledger-receipt.schema.json +45 -0
- package/schemas/v1.3.0/release-ledger.schema.json +60 -0
- package/schemas/v1.4.0/adapter-registry.schema.json +62 -0
- package/schemas/v1.4.0/evaluation-aggregate-report.schema.json +166 -0
- package/schemas/v1.4.0/evaluation-budget.schema.json +49 -0
- package/schemas/v1.4.0/evaluation-corpus.schema.json +93 -0
- package/schemas/v1.4.0/evaluation-fixture.schema.json +64 -0
- package/schemas/v1.4.0/evaluation-gate-policy.schema.json +33 -0
- package/schemas/v1.4.0/evaluation-grader-registration.schema.json +141 -0
- package/schemas/v1.4.0/evaluation-grader-registry.schema.json +20 -0
- package/schemas/v1.4.0/evaluation-host-profile-registry.schema.json +20 -0
- package/schemas/v1.4.0/evaluation-host-profile.schema.json +73 -0
- package/schemas/v1.4.0/evaluation-observation.schema.json +76 -0
- package/schemas/v1.4.0/evaluation-run-result.schema.json +84 -0
- package/schemas/v1.4.0/evaluation-scenario.schema.json +231 -0
- package/schemas/v1.4.0/evaluation-waiver.schema.json +65 -0
- package/schemas/v1.4.0/skill-certification-receipt.schema.json +103 -0
- package/schemas/v1.5.0/command-catalog.schema.json +296 -0
- package/schemas/v1.5.0/common.schema.json +261 -0
- package/schemas/v1.5.0/generated-asset-manifest.schema.json +313 -0
- package/schemas/v1.5.0/implementation-result.schema.json +152 -0
- package/schemas/v1.5.0/migration-preservation-manifest.schema.json +749 -0
- package/schemas/v1.5.0/output-catalog.schema.json +210 -0
- package/schemas/v1.5.0/output-path-catalog.schema.json +81 -0
- package/schemas/v1.5.0/role-registry.schema.json +261 -0
- package/schemas/v1.5.0/rule-catalog.schema.json +130 -0
- package/schemas/v1.5.0/skill-catalog.schema.json +303 -0
- package/schemas/v1.5.0/task-kind-registry.schema.json +167 -0
- package/schemas/v1.5.0/task-manifest.schema.json +373 -0
- package/schemas/v1.5.0/task-output-manifest.schema.json +462 -0
- package/schemas/v1.6.0/common.schema.json +462 -0
- package/schemas/v1.6.0/diagram-consumer-reference.schema.json +167 -0
- package/schemas/v1.6.0/diagram-document.schema.json +699 -0
- package/schemas/v1.6.0/diagram-fidelity-report.schema.json +137 -0
- package/schemas/v1.6.0/diagram-manifest.schema.json +162 -0
- package/schemas/v1.6.0/diagram-quality-report.schema.json +148 -0
- package/schemas/v1.6.0/diagram-review-binding.schema.json +132 -0
- package/schemas/v1.6.0/diagram-semantic-pattern-registry.schema.json +90 -0
- package/schemas/v1.6.0/diagram-type-registry.schema.json +373 -0
- package/schemas/v1.6.0/generated-asset-manifest.schema.json +103 -0
- package/schemas/v1.6.0/skill-catalog.schema.json +84 -0
- package/schemas/v1.6.0/skill-completion-receipt.schema.json +72 -0
- package/schemas/v1.6.0/skill-consent-record.schema.json +74 -0
- package/schemas/v1.6.0/skill-host-profile-registry.schema.json +526 -0
- package/schemas/v1.6.0/skill-learning-record.schema.json +116 -0
- package/schemas/v1.6.0/skill-module-registry.schema.json +108 -0
- package/schemas/v1.6.0/skill-routing-registry.schema.json +93 -0
- package/schemas/v1.6.0/skill-session.schema.json +73 -0
- package/schemas/v1.6.0/skill-source.schema.json +169 -0
- package/schemas/v1.7.0/common.schema.json +464 -0
- package/schemas/v1.7.0/generated-asset-manifest.schema.json +103 -0
- package/schemas/v1.7.0/planning-id-sequence.schema.json +50 -0
- package/schemas/v1.7.0/request-authority.schema.json +71 -0
- package/schemas/v1.7.0/skill-catalog.schema.json +84 -0
- package/schemas/v1.7.0/skill-completion-receipt.schema.json +72 -0
- package/schemas/v1.7.0/skill-consent-record.schema.json +74 -0
- package/schemas/v1.7.0/skill-host-profile-registry.schema.json +526 -0
- package/schemas/v1.7.0/skill-learning-record.schema.json +116 -0
- package/schemas/v1.7.0/skill-module-registry.schema.json +108 -0
- package/schemas/v1.7.0/skill-routing-registry.schema.json +93 -0
- package/schemas/v1.7.0/skill-session.schema.json +73 -0
- package/schemas/v1.7.0/skill-source.schema.json +169 -0
- package/schemas/v1.7.0/spec.schema.json +117 -0
- package/schemas/v1.7.0/story.schema.json +107 -0
- package/schemas/v1.7.0/task.schema.json +217 -0
- package/schemas/v1.8.0/common.schema.json +96 -0
- package/schemas/v1.8.0/skill-package.schema.json +63 -0
- package/schemas/v1.8.0/utility-command-catalog.schema.json +82 -0
- package/schemas/v1.9.0/design-document.schema.json +306 -0
- package/schemas/v1.9.0/design-review-bundle.schema.json +346 -0
- package/schemas/v1.9.0/design-review-workspace.schema.json +108 -0
- package/schemas/v1.9.0/design-workspace-create.schema.json +170 -0
- package/schemas/v1.9.0/design-workspace-event.schema.json +97 -0
- package/schemas/v1.9.0/design-workspace-revision.schema.json +56 -0
- package/schemas/v2.0.0/agent-runtime-manifest.schema.json +59 -0
- package/schemas/v2.0.0/business-operating-snapshot-projection.schema.json +14 -0
- package/schemas/v2.0.0/operate-allowed-action.schema.json +181 -0
- package/schemas/v2.0.0/operate-api-envelope.schema.json +225 -0
- package/schemas/v2.0.0/operate-capability-provider-registration.schema.json +30 -0
- package/schemas/v2.0.0/operate-domain-registration.schema.json +284 -0
- package/schemas/v2.0.0/operate-evidence-provider-registration.schema.json +55 -0
- package/schemas/v2.0.0/operate-evidence-resolver-registration.schema.json +46 -0
- package/schemas/v2.0.0/operate-executor-registration.schema.json +27 -0
- package/schemas/v2.0.0/operate-experience-live-patch.schema.json +47 -0
- package/schemas/v2.0.0/operate-experience-preview.schema.json +51 -0
- package/schemas/v2.0.0/operate-experience-view.schema.json +281 -0
- package/schemas/v2.0.0/operate-live-evidence-provider-registration.schema.json +48 -0
- package/schemas/v2.0.0/operate-live-evidence-provider-registry.schema.json +17 -0
- package/schemas/v2.0.0/operate-metric-provider-registration.schema.json +12 -0
- package/schemas/v2.0.0/operate-policy-provider-registration.schema.json +33 -0
- package/schemas/v2.0.0/operate-review-bound-submission.schema.json +29 -0
- package/schemas/v2.0.0/operate-review-display-workspace.schema.json +390 -0
- package/schemas/v2.0.0/operate-snapshot-provider-registration.schema.json +16 -0
- package/schemas/v2.0.0/operate-tool-call.schema.json +172 -0
- package/schemas/v2.0.0/operate-verification-provider-registration.schema.json +12 -0
- package/schemas/v2.0.0/operating-action-policy.schema.json +31 -0
- package/schemas/v2.0.0/operating-action-verification-plan.schema.json +13 -0
- package/schemas/v2.0.0/operating-action.schema.json +86 -0
- package/schemas/v2.0.0/operating-advisor-result.schema.json +394 -0
- package/schemas/v2.0.0/operating-approval-record.schema.json +23 -0
- package/schemas/v2.0.0/operating-approval-requirement.schema.json +31 -0
- package/schemas/v2.0.0/operating-artifact.schema.json +52 -0
- package/schemas/v2.0.0/operating-assignment.schema.json +437 -0
- package/schemas/v2.0.0/operating-assumption.schema.json +16 -0
- package/schemas/v2.0.0/operating-capability-availability.schema.json +15 -0
- package/schemas/v2.0.0/operating-capability-grant.schema.json +14 -0
- package/schemas/v2.0.0/operating-challenger-review.schema.json +204 -0
- package/schemas/v2.0.0/operating-checkpoint.schema.json +43 -0
- package/schemas/v2.0.0/operating-claim.schema.json +17 -0
- package/schemas/v2.0.0/operating-connector-checkpoint.schema.json +28 -0
- package/schemas/v2.0.0/operating-context-capture.schema.json +74 -0
- package/schemas/v2.0.0/operating-cycle-input-binding.schema.json +30 -0
- package/schemas/v2.0.0/operating-cycle.schema.json +42 -0
- package/schemas/v2.0.0/operating-decision-ledger.schema.json +237 -0
- package/schemas/v2.0.0/operating-decision.schema.json +137 -0
- package/schemas/v2.0.0/operating-delivery-evidence.schema.json +46 -0
- package/schemas/v2.0.0/operating-delivery-route.schema.json +43 -0
- package/schemas/v2.0.0/operating-delta.schema.json +13 -0
- package/schemas/v2.0.0/operating-domain-projection.schema.json +15 -0
- package/schemas/v2.0.0/operating-event-trigger.schema.json +11 -0
- package/schemas/v2.0.0/operating-event.schema.json +505 -0
- package/schemas/v2.0.0/operating-evidence-candidate.schema.json +88 -0
- package/schemas/v2.0.0/operating-evidence-edge.schema.json +39 -0
- package/schemas/v2.0.0/operating-evidence-graph.schema.json +25 -0
- package/schemas/v2.0.0/operating-evidence-observation.schema.json +48 -0
- package/schemas/v2.0.0/operating-evidence-ref.schema.json +51 -0
- package/schemas/v2.0.0/operating-evidence-resolution.schema.json +54 -0
- package/schemas/v2.0.0/operating-execution-result.schema.json +23 -0
- package/schemas/v2.0.0/operating-executive-board.schema.json +78 -0
- package/schemas/v2.0.0/operating-finding.schema.json +116 -0
- package/schemas/v2.0.0/operating-governed-operation.schema.json +54 -0
- package/schemas/v2.0.0/operating-intelligence-input-bundle.schema.json +128 -0
- package/schemas/v2.0.0/operating-intelligence-plan.schema.json +302 -0
- package/schemas/v2.0.0/operating-learning-receipt.schema.json +31 -0
- package/schemas/v2.0.0/operating-learning.schema.json +13 -0
- package/schemas/v2.0.0/operating-live-evidence-consent-record.schema.json +42 -0
- package/schemas/v2.0.0/operating-live-evidence-ingestion.schema.json +31 -0
- package/schemas/v2.0.0/operating-measurement-plan.schema.json +18 -0
- package/schemas/v2.0.0/operating-measurement-schedule-receipt.schema.json +7 -0
- package/schemas/v2.0.0/operating-measurement-schedule.schema.json +16 -0
- package/schemas/v2.0.0/operating-metric-observation.schema.json +15 -0
- package/schemas/v2.0.0/operating-metric.schema.json +18 -0
- package/schemas/v2.0.0/operating-model-state.schema.json +36 -0
- package/schemas/v2.0.0/operating-objective.schema.json +21 -0
- package/schemas/v2.0.0/operating-origin.schema.json +31 -0
- package/schemas/v2.0.0/operating-outcome-evaluation.schema.json +46 -0
- package/schemas/v2.0.0/operating-outcome.schema.json +13 -0
- package/schemas/v2.0.0/operating-planning-proposal.schema.json +45 -0
- package/schemas/v2.0.0/operating-policy-evaluation.schema.json +28 -0
- package/schemas/v2.0.0/operating-review-read.schema.json +217 -0
- package/schemas/v2.0.0/operating-review-receipt.schema.json +64 -0
- package/schemas/v2.0.0/operating-review.schema.json +47 -0
- package/schemas/v2.0.0/operating-risk.schema.json +96 -0
- package/schemas/v2.0.0/operating-rollback-plan.schema.json +10 -0
- package/schemas/v2.0.0/operating-rollback-result.schema.json +20 -0
- package/schemas/v2.0.0/operating-runtime-state.schema.json +289 -0
- package/schemas/v2.0.0/operating-scenario.schema.json +13 -0
- package/schemas/v2.0.0/operating-snapshot.schema.json +38 -0
- package/schemas/v2.0.0/operating-submission.schema.json +34 -0
- package/schemas/v2.0.0/operating-trace-matrix.schema.json +176 -0
- package/schemas/v2.0.0/operating-work-change-set.schema.json +89 -0
- package/schemas/v2.0.0/operating-work-ledger.schema.json +40 -0
- package/schemas/v2.0.0/software-operating-snapshot-projection.schema.json +14 -0
- package/src/browser-contracts.d.mts +5 -0
- package/src/browser-contracts.mjs +40 -0
- package/src/canonical-json.d.mts +5 -0
- package/src/canonical-json.mjs +142 -0
- package/src/compat/live-evidence-v2.mjs +1019 -0
- package/src/compat/operate-experience-live-patch.mjs +66 -0
- package/src/compat/operating-planning-contracts.mjs +114 -0
- package/src/contracts.d.mts +42 -0
- package/src/contracts.mjs +990 -0
- package/src/design-contracts.d.mts +62 -0
- package/src/design-contracts.mjs +186 -0
- package/src/design-publication-contracts.d.mts +4 -0
- package/src/design-publication-contracts.mjs +85 -0
- package/src/diagram-contracts.d.mts +19 -0
- package/src/diagram-contracts.mjs +28 -0
- package/src/enterprise-contracts.d.mts +107 -0
- package/src/enterprise-contracts.mjs +463 -0
- package/src/errors.d.mts +11 -0
- package/src/errors.mjs +109 -0
- package/src/generated/canonical-registries.mjs +5554 -0
- package/src/generated/contract-catalog-v2.mjs +7076 -0
- package/src/generated/diagram-registries.mjs +2529 -0
- package/src/index.d.mts +10 -0
- package/src/index.mjs +40 -0
- package/src/json-schema.d.mts +7 -0
- package/src/json-schema.mjs +283 -0
- package/src/planning-contracts.d.mts +10 -0
- package/src/planning-contracts.mjs +53 -0
- package/src/registries.d.mts +40 -0
- package/src/registries.mjs +261 -0
- package/src/review-experience-contracts.d.mts +11 -0
- package/src/review-experience-contracts.mjs +103 -0
- package/src/semver.d.mts +2 -0
- package/src/semver.mjs +7 -0
- package/src/skill-source-contracts.d.mts +25 -0
- package/src/skill-source-contracts.mjs +105 -0
- package/src/task-contracts.d.mts +7 -0
- package/src/task-contracts.mjs +130 -0
- package/src/workspace-contracts.d.mts +26 -0
- package/src/workspace-contracts.mjs +83 -0
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
import { assertProtocolArtifact } from '../contracts.mjs';
|
|
2
|
+
import { sha256Jcs } from '../canonical-json.mjs';
|
|
3
|
+
|
|
4
|
+
const HASH = /^sha256:[a-f0-9]{64}$/u;
|
|
5
|
+
const CHECKPOINT_TRANSITIONS = Object.freeze({
|
|
6
|
+
prepared: Object.freeze({ request: 'requesting', absent: 'absent' }),
|
|
7
|
+
requesting: Object.freeze({ materialize: 'materialized', absent: 'absent', uncertain: 'uncertain' }),
|
|
8
|
+
materialized: Object.freeze({ commit: 'committed', uncertain: 'uncertain' }),
|
|
9
|
+
committed: Object.freeze({}), absent: Object.freeze({}), uncertain: Object.freeze({}),
|
|
10
|
+
});
|
|
11
|
+
const SCHEDULE_TRANSITIONS = Object.freeze({
|
|
12
|
+
disabled: Object.freeze({ enable: 'enabled', revoke: 'revoked', expire: 'expired' }),
|
|
13
|
+
enabled: Object.freeze({ disable: 'disabled', pause: 'paused', revoke: 'revoked', expire: 'expired', complete: 'completed', 'run-issued': 'enabled' }),
|
|
14
|
+
paused: Object.freeze({ disable: 'disabled', resume: 'enabled', revoke: 'revoked', expire: 'expired' }),
|
|
15
|
+
revoked: Object.freeze({}), expired: Object.freeze({}), completed: Object.freeze({}),
|
|
16
|
+
});
|
|
17
|
+
const IMMUTABLE_CHECKPOINT_FIELDS = Object.freeze([
|
|
18
|
+
'checkpointId', 'requestId', 'requestHash', 'effect', 'provider', 'providerRegistrationHash',
|
|
19
|
+
'adapterVersion', 'sourceIdentityHash', 'credentialRefHash', 'boundsHash', 'queryHash',
|
|
20
|
+
'windowHash', 'redactionVersion', 'createdAt',
|
|
21
|
+
]);
|
|
22
|
+
const CONNECTOR_EVENT_FIELDS = Object.freeze([
|
|
23
|
+
'eventId', 'eventHash', 'type', 'expectedGeneration', 'expectedCheckpointHash', 'at', 'patch',
|
|
24
|
+
]);
|
|
25
|
+
const CONNECTOR_PATCH_FIELDS = Object.freeze({
|
|
26
|
+
request: Object.freeze(['consentRecordId', 'consentRecordHash', 'consentExpiresAt', 'runtimeCapabilityHash', 'health']),
|
|
27
|
+
materialize: Object.freeze(['artifactId', 'artifactHash', 'watermarkDigest', 'health']),
|
|
28
|
+
commit: Object.freeze(['watermarkDigest']),
|
|
29
|
+
absent: Object.freeze(['absenceDigest', 'health']),
|
|
30
|
+
uncertain: Object.freeze(['absenceDigest', 'health']),
|
|
31
|
+
});
|
|
32
|
+
const SCHEDULE_TRANSITION_FIELDS = Object.freeze([
|
|
33
|
+
'receiptId', 'type', 'expectedGeneration', 'expectedScheduleHash', 'expectedState',
|
|
34
|
+
'expectedRunCount', 'expectedNextEventIdentity', 'expectedNextDueAt', 'planHash',
|
|
35
|
+
'consentRecordHashes', 'ceilingsHash', 'clockHash', 'eventId', 'nextEventIdentity',
|
|
36
|
+
'nextDueAt', 'at',
|
|
37
|
+
]);
|
|
38
|
+
const SOURCE_CONTRACTS = Object.freeze([
|
|
39
|
+
'capacity-throughput', 'channel-economics', 'ci-test-evidence', 'competitor-positioning',
|
|
40
|
+
'context-manifest', 'demand-market', 'finance-metrics', 'incident-history',
|
|
41
|
+
'objective-metrics', 'operations-customer-health', 'planning-acceptance', 'prior-decisions',
|
|
42
|
+
'product-activation', 'repository-architecture', 'retention-discovery', 'support-incidents',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
export const LIVE_EVIDENCE_ABSENCE_KINDS_V2 = Object.freeze([
|
|
46
|
+
'insufficient-evidence', 'missing-consent', 'missing-credential', 'provider-unavailable',
|
|
47
|
+
'rate-limited', 'stale', 'unhealthy', 'unsupported',
|
|
48
|
+
]);
|
|
49
|
+
export const LIVE_EVIDENCE_EFFECT_CLASS_V2 = 'provider-call';
|
|
50
|
+
export const LIVE_EVIDENCE_PORTABLE_AUTHORITY_V2 = 'none';
|
|
51
|
+
export const OPERATING_OUTCOME_EVALUATION_OPERATORS_V2 = Object.freeze(['gt', 'gte', 'lt', 'lte', 'eq', 'between']);
|
|
52
|
+
|
|
53
|
+
export class OperatingLiveEvidenceErrorV2 extends Error {
|
|
54
|
+
constructor(code, message, details = {}) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = 'OperatingLiveEvidenceErrorV2';
|
|
57
|
+
this.code = code;
|
|
58
|
+
this.details = Object.freeze({ ...details });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fail(code, message, details) {
|
|
63
|
+
throw new OperatingLiveEvidenceErrorV2(code, message, details);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function clone(value) {
|
|
67
|
+
return structuredClone(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function freeze(value) {
|
|
71
|
+
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
72
|
+
for (const nested of Object.values(value)) freeze(nested);
|
|
73
|
+
Object.freeze(value);
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function safePortable(value, path = '$') {
|
|
79
|
+
if (typeof value === 'function' || typeof value === 'symbol' || typeof value === 'bigint') {
|
|
80
|
+
fail('LIVE_EVIDENCE_NON_PORTABLE', 'Live-evidence artifacts must be JSON-only.', { path });
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(value)) {
|
|
83
|
+
value.forEach((entry, index) => safePortable(entry, `${path}[${index}]`));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (value && typeof value === 'object') {
|
|
87
|
+
Object.entries(value).forEach(([key, entry]) => safePortable(entry, `${path}.${key}`));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (typeof value !== 'string') return;
|
|
91
|
+
if (value.includes('\u0000') || value.startsWith('/') || value.startsWith('~')
|
|
92
|
+
|| /(?:^|[\s"'`(])\/(?:Users|home|private|var|etc|tmp)\//u.test(value)
|
|
93
|
+
|| /(?:^|[\s"'`(])[A-Za-z]:[\\/][^\s]+/u.test(value)
|
|
94
|
+
|| value.split(/[\\/]/u).includes('..')
|
|
95
|
+
|| /-----BEGIN [A-Z ]*PRIVATE KEY-----/u.test(value)
|
|
96
|
+
|| /\b(?:Bearer|Basic)\s+[A-Za-z0-9+/=._-]+/u.test(value)
|
|
97
|
+
|| /\b(?:api[-_ ]?key|access[-_ ]?token|client[-_ ]?secret|password|passwd|secret|token)\s*[:=]\s*[^\s]+/iu.test(value)) {
|
|
98
|
+
fail('LIVE_EVIDENCE_UNSAFE', 'Live-evidence artifacts contain private path or credential material.', { path });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function protocol(kind, value, protocolVersion = '2.0.0') {
|
|
103
|
+
try {
|
|
104
|
+
assertProtocolArtifact(kind, value, { protocolVersion });
|
|
105
|
+
} catch (cause) {
|
|
106
|
+
fail('LIVE_EVIDENCE_CONTRACT_INVALID', `Invalid ${kind} contract.`, { cause: cause.code ?? cause.message });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function selfHash(record, field) {
|
|
111
|
+
const { [field]: claimed, ...body } = record;
|
|
112
|
+
const expected = sha256Jcs(body);
|
|
113
|
+
if (!HASH.test(claimed ?? '') || claimed !== expected) {
|
|
114
|
+
fail('LIVE_EVIDENCE_DIGEST_MISMATCH', `${field} does not bind the exact canonical record.`, { field, expected, actual: claimed ?? null });
|
|
115
|
+
}
|
|
116
|
+
return expected;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function exactDigest(record, expected, label) {
|
|
120
|
+
const actual = sha256Jcs(record);
|
|
121
|
+
if (actual !== expected) fail('LIVE_EVIDENCE_BASE_RECORD_MISMATCH', `${label} bytes do not match their bound digest.`, { expected, actual });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function same(left, right, label) {
|
|
125
|
+
if (sha256Jcs(left) !== sha256Jcs(right)) fail('LIVE_EVIDENCE_BINDING_MISMATCH', `${label} binding changed.`, { left, right });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function assertExactKeys(value, expected, label) {
|
|
129
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
130
|
+
fail('LIVE_EVIDENCE_CONTRACT_INVALID', `${label} must be one closed object.`);
|
|
131
|
+
}
|
|
132
|
+
const actual = Object.keys(value).sort();
|
|
133
|
+
const wanted = [...expected].sort();
|
|
134
|
+
if (!sameValue(actual, wanted)) {
|
|
135
|
+
fail('LIVE_EVIDENCE_CONTRACT_INVALID', `${label} contains missing or unsupported fields.`, { actual, expected: wanted });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function currentAt(timestamp, expiresAt, label) {
|
|
140
|
+
if (!(Date.parse(timestamp) < Date.parse(expiresAt))) {
|
|
141
|
+
fail('LIVE_EVIDENCE_STALE', `${label} is not current at the bound time.`, { timestamp, expiresAt });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function exactBaseRecord(options, singularKey, pluralKey, reference, identityFields, label) {
|
|
146
|
+
const supplied = [
|
|
147
|
+
options?.[singularKey],
|
|
148
|
+
...(Array.isArray(options?.[pluralKey]) ? options[pluralKey] : []),
|
|
149
|
+
].filter((entry) => entry !== undefined);
|
|
150
|
+
const matching = supplied.filter((entry) => identityFields.every((field) => entry?.[field] === reference[field]));
|
|
151
|
+
const unique = [...new Map(matching.map((entry) => [sha256Jcs(entry), entry])).values()];
|
|
152
|
+
if (unique.length !== 1) {
|
|
153
|
+
fail('LIVE_EVIDENCE_BASE_RECORD_REQUIRED', `${label} requires exactly one matching frozen base record.`, {
|
|
154
|
+
identity: Object.fromEntries(identityFields.map((field) => [field, reference[field]])),
|
|
155
|
+
matches: unique.length,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return unique[0];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function assertProviderBaseRecords(record, options = {}) {
|
|
162
|
+
const baseEvidenceProvider = exactBaseRecord(options, 'baseEvidenceProvider', 'baseEvidenceProviders',
|
|
163
|
+
record.baseEvidenceProvider, ['providerId', 'providerVersion'], 'Live provider registration');
|
|
164
|
+
protocol('operate-evidence-provider-registration', baseEvidenceProvider);
|
|
165
|
+
exactDigest(baseEvidenceProvider, record.baseEvidenceProvider.recordDigest, 'Evidence-provider registration');
|
|
166
|
+
const baseResolver = exactBaseRecord(options, 'baseResolver', 'baseResolvers',
|
|
167
|
+
record.baseResolver, ['resolverId', 'resolverVersion'], 'Live provider registration');
|
|
168
|
+
protocol('operate-evidence-resolver-registration', baseResolver);
|
|
169
|
+
exactDigest(baseResolver, record.baseResolver.recordDigest, 'Evidence-resolver registration');
|
|
170
|
+
if (!baseResolver.supportedEvidenceKinds.includes('operate-artifact')) {
|
|
171
|
+
fail('LIVE_EVIDENCE_BRIDGE_INVALID', 'The base resolver must use the canonical operate-artifact boundary.');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function assertLiveEvidenceProviderRegistrationStructureV2(record) {
|
|
176
|
+
protocol('operate-live-evidence-provider-registration', record);
|
|
177
|
+
safePortable(record);
|
|
178
|
+
selfHash(record, 'registrationHash');
|
|
179
|
+
if (record.effects.length !== 1 || record.effects[0] !== 'provider-call'
|
|
180
|
+
|| record.consent.required !== true || record.consent.mode !== 'named-owner-current'
|
|
181
|
+
|| record.consent.runtimeCapability !== 'opaque-local-preissued'
|
|
182
|
+
|| record.consent.noDefaultChoice !== true || record.consent.previewEffect !== 'none') {
|
|
183
|
+
fail('LIVE_EVIDENCE_AUTHORITY_INVALID', 'Registration must require current consent and a pre-issued opaque capability for provider-call.');
|
|
184
|
+
}
|
|
185
|
+
return freeze(clone(record));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function assertLiveEvidenceProviderRegistrationV2(record, options = {}) {
|
|
189
|
+
const normalized = assertLiveEvidenceProviderRegistrationStructureV2(record);
|
|
190
|
+
assertProviderBaseRecords(normalized, options);
|
|
191
|
+
return normalized;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function assertLiveEvidenceProviderRegistryV2(registry, options = {}) {
|
|
195
|
+
protocol('operate-live-evidence-provider-registry', registry);
|
|
196
|
+
safePortable(registry);
|
|
197
|
+
selfHash(registry, 'registryHash');
|
|
198
|
+
let previous = null;
|
|
199
|
+
const identities = new Set();
|
|
200
|
+
for (const provider of registry.providers) {
|
|
201
|
+
assertLiveEvidenceProviderRegistrationV2(provider, options);
|
|
202
|
+
const identity = `${provider.providerId}@${provider.providerVersion}`;
|
|
203
|
+
if (identities.has(identity) || (previous !== null && identity.localeCompare(previous) <= 0)) {
|
|
204
|
+
fail('LIVE_EVIDENCE_REGISTRY_CONFLICT', 'Provider registry membership must be unique and canonically ordered.', { identity });
|
|
205
|
+
}
|
|
206
|
+
identities.add(identity);
|
|
207
|
+
previous = identity;
|
|
208
|
+
}
|
|
209
|
+
return freeze(clone(registry));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function registerLiveEvidenceProviderV2(registry, registration, options = {}) {
|
|
213
|
+
const current = assertLiveEvidenceProviderRegistryV2(registry, options);
|
|
214
|
+
const provider = assertLiveEvidenceProviderRegistrationV2(registration, options);
|
|
215
|
+
const index = current.providers.findIndex((entry) => entry.providerId === provider.providerId && entry.providerVersion === provider.providerVersion);
|
|
216
|
+
if (index >= 0) {
|
|
217
|
+
if (current.providers[index].registrationHash !== provider.registrationHash) {
|
|
218
|
+
fail('LIVE_EVIDENCE_REGISTRY_CONFLICT', 'Divergent provider bytes cannot reuse one provider identity.');
|
|
219
|
+
}
|
|
220
|
+
return current;
|
|
221
|
+
}
|
|
222
|
+
const next = clone(current);
|
|
223
|
+
next.providers.push(clone(provider));
|
|
224
|
+
next.providers.sort((left, right) => `${left.providerId}@${left.providerVersion}`.localeCompare(`${right.providerId}@${right.providerVersion}`));
|
|
225
|
+
next.registryHash = sha256Jcs(Object.fromEntries(Object.entries(next).filter(([key]) => key !== 'registryHash')));
|
|
226
|
+
return assertLiveEvidenceProviderRegistryV2(next, options);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function assertOperatingLiveEvidenceConsentRecordV2(record) {
|
|
230
|
+
protocol('operating-live-evidence-consent-record', record);
|
|
231
|
+
safePortable(record);
|
|
232
|
+
selfHash(record, 'consentHash');
|
|
233
|
+
if (record.authority !== 'none' || record.docket.defaultChoice !== null || record.docket.cancelEffect !== 'none') {
|
|
234
|
+
fail('LIVE_EVIDENCE_AUTHORITY_INVALID', 'Consent proof and docket cannot grant access authority or preselect an owner choice.');
|
|
235
|
+
}
|
|
236
|
+
if (sha256Jcs(record.docket) !== record.docketHash) fail('LIVE_EVIDENCE_DIGEST_MISMATCH', 'Consent docket bytes changed.');
|
|
237
|
+
same(record.docket.provider, record.provider, 'Consent provider');
|
|
238
|
+
for (const field of ['adapterVersion', 'accountIdentityHash', 'selectors', 'scopes', 'queryHash', 'window', 'ceilings', 'classification', 'purpose', 'expiresAt']) {
|
|
239
|
+
same(record.docket[field], record[field], `Consent ${field}`);
|
|
240
|
+
}
|
|
241
|
+
currentAt(record.issuedAt, record.expiresAt, 'Consent');
|
|
242
|
+
return freeze(clone(record));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function assertOperatingLiveEvidenceIngestionV2(record, {
|
|
246
|
+
liveProviderRegistration, baseEvidenceProvider, baseResolver, assignment,
|
|
247
|
+
consentRecord, connectorCheckpoint,
|
|
248
|
+
} = {}) {
|
|
249
|
+
protocol('operating-live-evidence-ingestion', record);
|
|
250
|
+
safePortable(record);
|
|
251
|
+
selfHash(record, 'ingestionHash');
|
|
252
|
+
if (!SOURCE_CONTRACTS.includes(record.sourceContract.id)) fail('LIVE_EVIDENCE_SOURCE_CONTRACT_INVALID', 'Ingestion source contract is not accepted by the frozen operate-artifact resolver.');
|
|
253
|
+
if (record.status === 'materialized') {
|
|
254
|
+
if (record.health.status !== 'available' || record.absences.length !== 0 || record.records.length === 0) {
|
|
255
|
+
fail('LIVE_EVIDENCE_FALSE_PASS', 'Materialized evidence requires available health, records, and no absence.');
|
|
256
|
+
}
|
|
257
|
+
if (record.materialization.contentDigest !== deriveOperatingLiveEvidenceContentDigestV2(record)) {
|
|
258
|
+
fail('LIVE_EVIDENCE_DIGEST_MISMATCH', 'Materialization contentDigest does not bind the exact redacted record manifest.');
|
|
259
|
+
}
|
|
260
|
+
currentAt(record.sourceTiming.capturedAt, record.health.freshUntil, 'Provider health');
|
|
261
|
+
currentAt(record.sourceTiming.capturedAt, record.sourceTiming.freshUntil, 'Source evidence');
|
|
262
|
+
}
|
|
263
|
+
if (record.assignment.requestHash !== record.requestHash || record.submission.operation !== 'operate.assignment.submit') {
|
|
264
|
+
fail('LIVE_EVIDENCE_BRIDGE_INVALID', 'Ingestion must bind its issued Assignment request and normal submission operation.');
|
|
265
|
+
}
|
|
266
|
+
if (record.assignment.assignmentRevision !== 1) {
|
|
267
|
+
fail('LIVE_EVIDENCE_BRIDGE_INVALID', 'The immutable Protocol 2.0 Assignment revision must be one.');
|
|
268
|
+
}
|
|
269
|
+
if (record.materialization !== null) {
|
|
270
|
+
for (const field of ['artifactId', 'artifactCreatedEventId', 'evidenceRefId']) same(record.materialization[field], record.submission[field], `Materialization ${field}`);
|
|
271
|
+
}
|
|
272
|
+
for (const candidate of record.evidenceCandidates) {
|
|
273
|
+
if (candidate.evidenceKind !== 'operate-artifact' || candidate.sourceArtifactId !== record.submission.artifactId
|
|
274
|
+
|| candidate.locator?.artifactId !== record.submission.artifactId
|
|
275
|
+
|| candidate.locator?.expectedArtifactType !== 'live-evidence-ingestion'
|
|
276
|
+
|| candidate.locator?.expectedSchemaId !== 'operating-live-evidence-ingestion'
|
|
277
|
+
|| candidate.locator?.expectedSchemaVersion !== '2.0.0'
|
|
278
|
+
|| candidate.provider.id !== record.providerRegistration.providerId
|
|
279
|
+
|| candidate.provider.version !== record.providerRegistration.providerVersion
|
|
280
|
+
|| candidate.resolver.id !== record.resolverRegistration.resolverId
|
|
281
|
+
|| candidate.resolver.version !== record.resolverRegistration.resolverVersion) {
|
|
282
|
+
fail('LIVE_EVIDENCE_BRIDGE_INVALID', 'Evidence candidates must reference the pre-issued accepted ingestion Artifact.');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const link of record.evidenceClaimLinks) {
|
|
286
|
+
if (link.sourceArtifactId !== record.submission.artifactId
|
|
287
|
+
|| !record.evidenceCandidates.some((candidate) => candidate.candidateId === link.candidateId)) {
|
|
288
|
+
fail('LIVE_EVIDENCE_BRIDGE_INVALID', 'Evidence claim links must bind a declared candidate and accepted source Artifact.');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (liveProviderRegistration !== undefined) {
|
|
292
|
+
const registration = assertLiveEvidenceProviderRegistrationV2(liveProviderRegistration, { baseEvidenceProvider, baseResolver });
|
|
293
|
+
if (registration.registrationHash !== record.providerRegistrationHash) fail('LIVE_EVIDENCE_BASE_RECORD_MISMATCH', 'Live provider registration changed.');
|
|
294
|
+
same([registration.providerId, registration.providerVersion, registration.adapterVersion], [record.provider.id, record.provider.version, record.adapterVersion], 'Live provider');
|
|
295
|
+
same(registration.baseEvidenceProvider, record.providerRegistration, 'Base evidence-provider reference');
|
|
296
|
+
same(registration.baseResolver, record.resolverRegistration, 'Base evidence-resolver reference');
|
|
297
|
+
}
|
|
298
|
+
if (record.resolverRegistrationHash !== record.resolverRegistration.recordDigest) fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Resolver registration digest fields disagree.');
|
|
299
|
+
if (baseEvidenceProvider !== undefined) {
|
|
300
|
+
protocol('operate-evidence-provider-registration', baseEvidenceProvider);
|
|
301
|
+
exactDigest(baseEvidenceProvider, record.providerRegistration.recordDigest, 'Base evidence-provider registration');
|
|
302
|
+
}
|
|
303
|
+
if (baseResolver !== undefined) {
|
|
304
|
+
protocol('operate-evidence-resolver-registration', baseResolver);
|
|
305
|
+
exactDigest(baseResolver, record.resolverRegistration.recordDigest, 'Base evidence-resolver registration');
|
|
306
|
+
if (!baseResolver.supportedSourceContracts.some((entry) => sameValue(entry, record.sourceContract))) {
|
|
307
|
+
fail('LIVE_EVIDENCE_SOURCE_CONTRACT_INVALID', 'The selected resolver does not support the ingestion source contract.');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (assignment !== undefined) {
|
|
311
|
+
protocol('operating-assignment', assignment);
|
|
312
|
+
exactDigest(assignment, record.assignment.assignmentHash, 'Issued Assignment');
|
|
313
|
+
if (assignment.assignmentId !== record.assignment.assignmentId || assignment.assignmentKind !== 'verification'
|
|
314
|
+
|| assignment.outputContract.schemaId !== 'operating-live-evidence-ingestion'
|
|
315
|
+
|| assignment.outputContract.schemaVersion !== '2.0.0') {
|
|
316
|
+
fail('LIVE_EVIDENCE_BRIDGE_INVALID', 'The canonical Assignment does not authorize this ingestion output.');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (record.status === 'materialized' && (assignment === undefined || consentRecord === undefined
|
|
320
|
+
|| connectorCheckpoint === undefined || liveProviderRegistration === undefined
|
|
321
|
+
|| baseEvidenceProvider === undefined || baseResolver === undefined)) {
|
|
322
|
+
fail('LIVE_EVIDENCE_BASE_RECORD_REQUIRED', 'Materialized ingestion requires exact Assignment, consent, checkpoint, and provider registrations.');
|
|
323
|
+
}
|
|
324
|
+
if (consentRecord !== undefined) {
|
|
325
|
+
const consent = assertOperatingLiveEvidenceConsentRecordV2(consentRecord);
|
|
326
|
+
if (consent.consentHash !== record.consentRecordHash) fail('LIVE_EVIDENCE_BASE_RECORD_MISMATCH', 'Consent record changed.');
|
|
327
|
+
same([consent.provider, consent.adapterVersion, consent.classification], [record.provider, record.adapterVersion, record.classification], 'Consent provider');
|
|
328
|
+
if (sha256Jcs(consent.ceilings) !== record.boundsHash) fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Consent ceilings changed.');
|
|
329
|
+
currentAt(record.sourceTiming.capturedAt, consent.expiresAt, 'Consent');
|
|
330
|
+
}
|
|
331
|
+
if (connectorCheckpoint !== undefined) {
|
|
332
|
+
const checkpoint = assertOperatingConnectorCheckpointV2(connectorCheckpoint);
|
|
333
|
+
if (checkpoint.state !== 'requesting' || checkpoint.checkpointId !== record.checkpoint.checkpointId
|
|
334
|
+
|| checkpoint.checkpointHash !== record.checkpoint.checkpointHash
|
|
335
|
+
|| checkpoint.predecessorDigest !== record.checkpoint.predecessorDigest
|
|
336
|
+
|| checkpoint.requestId !== record.requestId || checkpoint.providerRegistrationHash !== record.providerRegistrationHash
|
|
337
|
+
|| checkpoint.consentRecordHash !== record.consentRecordHash
|
|
338
|
+
|| checkpoint.runtimeCapabilityHash !== record.runtimeCapabilityHash
|
|
339
|
+
|| checkpoint.boundsHash !== record.boundsHash || !sameValue(checkpoint.provider, record.provider)
|
|
340
|
+
|| checkpoint.adapterVersion !== record.adapterVersion || !sameValue(checkpoint.health, record.health)) {
|
|
341
|
+
fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Ingestion changed its exact requesting connector checkpoint.');
|
|
342
|
+
}
|
|
343
|
+
if (consentRecord !== undefined && (checkpoint.queryHash !== consentRecord.queryHash
|
|
344
|
+
|| checkpoint.windowHash !== sha256Jcs(consentRecord.window)
|
|
345
|
+
|| checkpoint.boundsHash !== sha256Jcs(consentRecord.ceilings))) {
|
|
346
|
+
fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Checkpoint query, window, or bounds changed from owner consent.');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (assignment !== undefined && consentRecord !== undefined && connectorCheckpoint !== undefined) {
|
|
350
|
+
const expectedRequestHash = deriveOperatingLiveEvidenceRequestHashV2({
|
|
351
|
+
assignment, consentRecord, connectorCheckpoint, liveProviderRegistration, sourceContract: record.sourceContract,
|
|
352
|
+
baseEvidenceProvider, baseResolver, classification: record.classification,
|
|
353
|
+
});
|
|
354
|
+
if (record.requestHash !== expectedRequestHash || connectorCheckpoint.requestHash !== expectedRequestHash) {
|
|
355
|
+
fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Ingestion request is not the engine-derived Assignment/provider request.');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return freeze(clone(record));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function sameValue(left, right) {
|
|
362
|
+
return sha256Jcs(left) === sha256Jcs(right);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function deriveOperatingLiveEvidenceRequestHashV2({
|
|
366
|
+
assignment, consentRecord, connectorCheckpoint, liveProviderRegistration, baseEvidenceProvider, baseResolver,
|
|
367
|
+
sourceContract, classification,
|
|
368
|
+
}) {
|
|
369
|
+
protocol('operating-assignment', assignment);
|
|
370
|
+
const consent = assertOperatingLiveEvidenceConsentRecordV2(consentRecord);
|
|
371
|
+
const checkpoint = assertOperatingConnectorCheckpointV2(connectorCheckpoint);
|
|
372
|
+
const registration = assertLiveEvidenceProviderRegistrationV2(liveProviderRegistration, {
|
|
373
|
+
baseEvidenceProvider, baseResolver,
|
|
374
|
+
});
|
|
375
|
+
return sha256Jcs({
|
|
376
|
+
domain: 'openplanr-operating-live-evidence-request-v2',
|
|
377
|
+
assignment: { assignmentId: assignment.assignmentId, assignmentHash: sha256Jcs(assignment) },
|
|
378
|
+
requestId: checkpoint.requestId,
|
|
379
|
+
provider: checkpoint.provider,
|
|
380
|
+
providerRegistrationHash: registration.registrationHash,
|
|
381
|
+
adapterVersion: checkpoint.adapterVersion,
|
|
382
|
+
consentRecordHash: consent.consentHash,
|
|
383
|
+
runtimeCapabilityHash: checkpoint.runtimeCapabilityHash,
|
|
384
|
+
sourceIdentityHash: checkpoint.sourceIdentityHash,
|
|
385
|
+
credentialRefHash: checkpoint.credentialRefHash,
|
|
386
|
+
boundsHash: checkpoint.boundsHash,
|
|
387
|
+
queryHash: checkpoint.queryHash,
|
|
388
|
+
windowHash: checkpoint.windowHash,
|
|
389
|
+
sourceContract,
|
|
390
|
+
classification,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function deriveOperatingLiveEvidenceContentDigestV2(record) {
|
|
395
|
+
if (record?.materialization === null || record?.materialization === undefined || !Array.isArray(record?.records)) {
|
|
396
|
+
fail('LIVE_EVIDENCE_CONTRACT_INVALID', 'Materialized live evidence requires a redacted record manifest.');
|
|
397
|
+
}
|
|
398
|
+
const records = clone(record.records).sort((left, right) => left.recordIdentityHash.localeCompare(right.recordIdentityHash));
|
|
399
|
+
return sha256Jcs({
|
|
400
|
+
domain: 'openplanr-operating-live-evidence-redacted-content-v2',
|
|
401
|
+
redactionVersion: record.materialization.redactionVersion,
|
|
402
|
+
records,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function assertAcceptedLiveEvidenceSourceV2(record, {
|
|
407
|
+
assignment, artifact, submittedEvent, artifactCreatedEvent, validatedEvent,
|
|
408
|
+
...options
|
|
409
|
+
}) {
|
|
410
|
+
const ingestion = assertOperatingLiveEvidenceIngestionV2(record, { assignment, ...options });
|
|
411
|
+
protocol('operating-artifact', artifact);
|
|
412
|
+
protocol('operating-event', submittedEvent);
|
|
413
|
+
protocol('operating-event', artifactCreatedEvent);
|
|
414
|
+
protocol('operating-event', validatedEvent);
|
|
415
|
+
if (artifact.artifactId !== ingestion.submission.artifactId || artifact.assignmentId !== ingestion.assignment.assignmentId
|
|
416
|
+
|| artifact.artifactType !== 'live-evidence-ingestion'
|
|
417
|
+
|| artifact.schemaId !== 'operating-live-evidence-ingestion' || artifact.artifactSchemaVersion !== '2.0.0'
|
|
418
|
+
|| artifact.canonicalHash !== sha256Jcs(ingestion)) {
|
|
419
|
+
fail('LIVE_EVIDENCE_BRIDGE_INVALID', 'Accepted Artifact does not bind the exact ingestion bytes and Assignment.');
|
|
420
|
+
}
|
|
421
|
+
const eventBindings = [[submittedEvent, 'assignment.submitted', ingestion.submission.submittedEventId], [artifactCreatedEvent, 'artifact.created', ingestion.submission.artifactCreatedEventId], [validatedEvent, 'assignment.validated', ingestion.submission.validatedEventId]];
|
|
422
|
+
for (const [event, type, eventId] of eventBindings) {
|
|
423
|
+
selfHash(event, 'eventHash');
|
|
424
|
+
if (event.eventId !== eventId || event.type !== type || event.cycleId !== artifact.cycleId
|
|
425
|
+
|| event.timestamp !== artifact.createdAt || event.actor.kind !== 'runtime' || event.actor.id !== 'openplanr') {
|
|
426
|
+
fail('LIVE_EVIDENCE_BRIDGE_INVALID', 'Accepted Event lineage does not match the pre-issued ingestion identities.', { type });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (submittedEvent.entityId !== assignment.assignmentId || artifactCreatedEvent.entityId !== artifact.artifactId
|
|
430
|
+
|| validatedEvent.entityId !== assignment.assignmentId
|
|
431
|
+
|| artifactCreatedEvent.causationId !== submittedEvent.eventId || validatedEvent.causationId !== artifactCreatedEvent.eventId
|
|
432
|
+
|| submittedEvent.correlationId !== artifactCreatedEvent.correlationId || submittedEvent.correlationId !== validatedEvent.correlationId
|
|
433
|
+
|| artifactCreatedEvent.previousEventHash !== submittedEvent.eventHash || validatedEvent.previousEventHash !== artifactCreatedEvent.eventHash
|
|
434
|
+
|| artifactCreatedEvent.sequence !== submittedEvent.sequence + 1 || validatedEvent.sequence !== artifactCreatedEvent.sequence + 1
|
|
435
|
+
|| !sameValue(submittedEvent.payload, {
|
|
436
|
+
assignmentId: assignment.assignmentId, submissionId: ingestion.submission.submissionId,
|
|
437
|
+
rawHash: artifact.rawHash, canonicalHash: artifact.canonicalHash, sizeBytes: artifact.sizeBytes,
|
|
438
|
+
mediaType: artifact.mediaType, encoding: artifact.encoding,
|
|
439
|
+
}) || !sameValue(artifactCreatedEvent.payload, artifact)
|
|
440
|
+
|| validatedEvent.payload.assignmentId !== assignment.assignmentId
|
|
441
|
+
|| validatedEvent.payload.submissionId !== ingestion.submission.submissionId
|
|
442
|
+
|| validatedEvent.payload.artifactId !== artifact.artifactId
|
|
443
|
+
|| typeof validatedEvent.payload.validatorVersion !== 'string') {
|
|
444
|
+
fail('LIVE_EVIDENCE_BRIDGE_INVALID', 'Runtime-created Assignment/Event payload or chain changed.');
|
|
445
|
+
}
|
|
446
|
+
return ingestion;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function assertAcceptedLiveEvidenceBridgeV2(record, {
|
|
450
|
+
assignment, artifact, submittedEvent, artifactCreatedEvent, validatedEvent, evidenceRef,
|
|
451
|
+
evidenceArtifact, evidenceResolution, resultCheckpoint, checkpointHistory = [],
|
|
452
|
+
...options
|
|
453
|
+
}) {
|
|
454
|
+
const ingestion = assertAcceptedLiveEvidenceSourceV2(record, {
|
|
455
|
+
assignment, artifact, submittedEvent, artifactCreatedEvent, validatedEvent, ...options,
|
|
456
|
+
});
|
|
457
|
+
protocol('operating-evidence-ref', evidenceRef);
|
|
458
|
+
protocol('operating-artifact', evidenceArtifact);
|
|
459
|
+
protocol('operating-evidence-resolution', evidenceResolution);
|
|
460
|
+
const candidate = ingestion.evidenceCandidates.find((entry) => entry.candidateId === evidenceRef.candidateId);
|
|
461
|
+
const expectedLocator = candidate === undefined ? null : {
|
|
462
|
+
artifactId: candidate.locator.artifactId,
|
|
463
|
+
expectedArtifactType: candidate.locator.expectedArtifactType,
|
|
464
|
+
expectedSchemaId: candidate.locator.expectedSchemaId,
|
|
465
|
+
expectedSchemaVersion: candidate.locator.expectedSchemaVersion,
|
|
466
|
+
expectedRawHash: artifact.rawHash,
|
|
467
|
+
expectedCanonicalHash: artifact.canonicalHash,
|
|
468
|
+
};
|
|
469
|
+
if (evidenceRef.evidenceRefId !== ingestion.submission.evidenceRefId || evidenceRef.sourceArtifactId !== artifact.artifactId
|
|
470
|
+
|| candidate === undefined || evidenceRef.evidenceKind !== 'operate-artifact'
|
|
471
|
+
|| !sameValue(evidenceRef.sourceContract, ingestion.sourceContract)
|
|
472
|
+
|| (candidate.locator.expectedRawHash !== undefined && candidate.locator.expectedRawHash !== artifact.rawHash)
|
|
473
|
+
|| (candidate.locator.expectedCanonicalHash !== undefined && candidate.locator.expectedCanonicalHash !== artifact.canonicalHash)
|
|
474
|
+
|| !sameValue(evidenceRef.locator, expectedLocator)
|
|
475
|
+
|| !sameValue(evidenceRef.provider, candidate.provider) || !sameValue(evidenceRef.resolver, candidate.resolver)
|
|
476
|
+
|| evidenceRef.classification !== ingestion.classification
|
|
477
|
+
|| evidenceRef.evidenceArtifactId !== evidenceArtifact.artifactId
|
|
478
|
+
|| evidenceRef.evidenceArtifactRawHash !== evidenceArtifact.rawHash
|
|
479
|
+
|| evidenceRef.evidenceArtifactCanonicalHash !== evidenceArtifact.canonicalHash
|
|
480
|
+
|| evidenceArtifact.artifactId === artifact.artifactId
|
|
481
|
+
|| evidenceArtifact.artifactType !== 'evidence-snapshot'
|
|
482
|
+
|| evidenceArtifact.assignmentId !== artifact.assignmentId || evidenceArtifact.cycleId !== artifact.cycleId
|
|
483
|
+
|| evidenceArtifact.scopeId !== artifact.scopeId || evidenceArtifact.domainId !== artifact.domainId
|
|
484
|
+
|| evidenceArtifact.domainVersion !== artifact.domainVersion
|
|
485
|
+
|| evidenceArtifact.schemaId !== 'operating-evidence-snapshot' || evidenceArtifact.artifactSchemaVersion !== '1.0.0'
|
|
486
|
+
|| evidenceArtifact.mediaType !== 'application/octet-stream' || evidenceArtifact.encoding !== 'binary'
|
|
487
|
+
|| evidenceArtifact.rawHash !== artifact.rawHash || evidenceArtifact.canonicalHash !== artifact.canonicalHash
|
|
488
|
+
|| evidenceArtifact.sizeBytes !== artifact.sizeBytes || evidenceArtifact.sensitivity !== artifact.sensitivity
|
|
489
|
+
|| evidenceArtifact.retentionClass !== artifact.retentionClass
|
|
490
|
+
|| !sameValue(evidenceArtifact.inputArtifactIds, [artifact.artifactId])
|
|
491
|
+
|| evidenceArtifact.createdAt !== evidenceRef.resolvedAt
|
|
492
|
+
|| !sameValue(evidenceArtifact.producer, { actorId: 'openplanr', roleId: 'evidence-resolver', runtime: 'openplanr' })) {
|
|
493
|
+
fail('LIVE_EVIDENCE_BRIDGE_INVALID', 'Accepted EvidenceRef does not bind the ingestion Artifact.');
|
|
494
|
+
}
|
|
495
|
+
if (evidenceResolution.outcome !== 'resolved' || evidenceResolution.error !== null
|
|
496
|
+
|| evidenceResolution.candidateId !== candidate.candidateId
|
|
497
|
+
|| evidenceResolution.sourceArtifactId !== artifact.artifactId
|
|
498
|
+
|| evidenceResolution.evidenceKind !== 'operate-artifact'
|
|
499
|
+
|| !sameValue(evidenceResolution.sourceContract, ingestion.sourceContract)
|
|
500
|
+
|| !sameValue(evidenceResolution.provider, candidate.provider)
|
|
501
|
+
|| !sameValue(evidenceResolution.resolver, candidate.resolver)
|
|
502
|
+
|| evidenceResolution.evidenceRefId !== evidenceRef.evidenceRefId
|
|
503
|
+
|| evidenceResolution.evidenceArtifactId !== evidenceArtifact.artifactId
|
|
504
|
+
|| evidenceResolution.resolvedAt !== evidenceRef.resolvedAt
|
|
505
|
+
|| evidenceArtifact.createdAt !== evidenceResolution.resolvedAt
|
|
506
|
+
|| evidenceResolution.scopeId !== artifact.scopeId || evidenceResolution.domainId !== artifact.domainId
|
|
507
|
+
|| evidenceResolution.domainVersion !== artifact.domainVersion) {
|
|
508
|
+
fail('LIVE_EVIDENCE_BRIDGE_INVALID', 'Evidence resolution changed the runtime-built snapshot or EvidenceRef custody.');
|
|
509
|
+
}
|
|
510
|
+
if (resultCheckpoint === undefined) fail('LIVE_EVIDENCE_BASE_RECORD_REQUIRED', 'Accepted ingestion requires the exact post-acceptance connector checkpoint.');
|
|
511
|
+
const requestCheckpoint = assertOperatingConnectorCheckpointV2(options.connectorCheckpoint);
|
|
512
|
+
const chain = [requestCheckpoint, ...checkpointHistory.map(assertOperatingConnectorCheckpointV2), assertOperatingConnectorCheckpointV2(resultCheckpoint)];
|
|
513
|
+
for (let index = 1; index < chain.length; index += 1) {
|
|
514
|
+
if (chain[index].predecessorDigest !== chain[index - 1].checkpointHash
|
|
515
|
+
|| chain[index].generation !== chain[index - 1].generation + 1) fail('LIVE_EVIDENCE_BRIDGE_INVALID', 'Connector checkpoint lineage is not contiguous.');
|
|
516
|
+
}
|
|
517
|
+
const terminalCheckpoint = chain.at(-1);
|
|
518
|
+
if (!['materialized', 'committed'].includes(terminalCheckpoint.state)
|
|
519
|
+
|| terminalCheckpoint.artifactId !== artifact.artifactId || terminalCheckpoint.artifactHash !== artifact.canonicalHash
|
|
520
|
+
|| terminalCheckpoint.requestHash !== ingestion.requestHash
|
|
521
|
+
|| terminalCheckpoint.consentRecordHash !== ingestion.consentRecordHash
|
|
522
|
+
|| terminalCheckpoint.runtimeCapabilityHash !== ingestion.runtimeCapabilityHash
|
|
523
|
+
|| !sameValue(terminalCheckpoint.health, ingestion.health)) {
|
|
524
|
+
fail('LIVE_EVIDENCE_BRIDGE_INVALID', 'Post-acceptance checkpoint changed Artifact or request custody.');
|
|
525
|
+
}
|
|
526
|
+
return ingestion;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export function assertOperatingConnectorCheckpointV2(checkpoint) {
|
|
530
|
+
protocol('operating-connector-checkpoint', checkpoint);
|
|
531
|
+
safePortable(checkpoint);
|
|
532
|
+
selfHash(checkpoint, 'checkpointHash');
|
|
533
|
+
if (checkpoint.generation === 0 && (checkpoint.state !== 'prepared' || checkpoint.predecessorDigest !== null || checkpoint.lastEventId !== null || checkpoint.lastEventHash !== null)) {
|
|
534
|
+
fail('LIVE_EVIDENCE_CHECKPOINT_INVALID', 'Generation-zero connector checkpoint must be an unadvanced prepared record.');
|
|
535
|
+
}
|
|
536
|
+
if (checkpoint.state === 'prepared' && (checkpoint.consentRecordId !== null || checkpoint.consentRecordHash !== null
|
|
537
|
+
|| checkpoint.consentExpiresAt !== null || checkpoint.runtimeCapabilityHash !== null
|
|
538
|
+
|| checkpoint.artifactId !== null || checkpoint.artifactHash !== null || checkpoint.absenceDigest !== null)) {
|
|
539
|
+
fail('LIVE_EVIDENCE_CHECKPOINT_INVALID', 'Prepared checkpoint cannot claim consent, capability, Artifact, or absence custody.');
|
|
540
|
+
}
|
|
541
|
+
if (['requesting', 'materialized', 'committed'].includes(checkpoint.state)) {
|
|
542
|
+
if (checkpoint.consentRecordId === null || checkpoint.consentRecordHash === null
|
|
543
|
+
|| checkpoint.consentExpiresAt === null || checkpoint.runtimeCapabilityHash === null) {
|
|
544
|
+
fail('LIVE_EVIDENCE_CHECKPOINT_INVALID', 'Requested checkpoint requires exact consent and runtime capability custody.');
|
|
545
|
+
}
|
|
546
|
+
currentAt(checkpoint.updatedAt, checkpoint.consentExpiresAt, 'Connector consent');
|
|
547
|
+
}
|
|
548
|
+
if (['materialized', 'committed'].includes(checkpoint.state)) {
|
|
549
|
+
if (checkpoint.health.status !== 'available' || checkpoint.artifactId === null || checkpoint.artifactHash === null
|
|
550
|
+
|| checkpoint.absenceDigest !== null) fail('LIVE_EVIDENCE_FALSE_PASS', 'Materialized connector checkpoint requires available health and exact Artifact custody.');
|
|
551
|
+
currentAt(checkpoint.updatedAt, checkpoint.health.freshUntil, 'Provider health');
|
|
552
|
+
}
|
|
553
|
+
if (['absent', 'uncertain'].includes(checkpoint.state)
|
|
554
|
+
&& (checkpoint.absenceDigest === null || checkpoint.artifactId !== null || checkpoint.artifactHash !== null)) {
|
|
555
|
+
fail('LIVE_EVIDENCE_CHECKPOINT_INVALID', 'Absent or uncertain checkpoint requires one typed absence and no Artifact custody.');
|
|
556
|
+
}
|
|
557
|
+
return freeze(clone(checkpoint));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export function reduceOperatingConnectorCheckpointV2(currentValue, eventValue) {
|
|
561
|
+
const current = assertOperatingConnectorCheckpointV2(currentValue);
|
|
562
|
+
const event = clone(eventValue);
|
|
563
|
+
assertExactKeys(event, CONNECTOR_EVENT_FIELDS, 'Connector transition Event');
|
|
564
|
+
if (!CONNECTOR_PATCH_FIELDS[event.type]) fail('LIVE_EVIDENCE_TRANSITION_INVALID', 'Unknown connector transition type.', { type: event.type });
|
|
565
|
+
assertExactKeys(event.patch, CONNECTOR_PATCH_FIELDS[event.type], `${event.type} connector patch`);
|
|
566
|
+
const claimed = event.eventHash;
|
|
567
|
+
delete event.eventHash;
|
|
568
|
+
const eventHash = sha256Jcs(event);
|
|
569
|
+
if (claimed !== eventHash) fail('LIVE_EVIDENCE_DIGEST_MISMATCH', 'Connector event hash does not bind the exact event.');
|
|
570
|
+
if (current.lastEventId === event.eventId) {
|
|
571
|
+
if (current.lastEventHash !== eventHash) fail('LIVE_EVIDENCE_REPLAY_CONFLICT', 'Divergent connector event reused one identity.');
|
|
572
|
+
return current;
|
|
573
|
+
}
|
|
574
|
+
if (event.expectedGeneration !== current.generation || event.expectedCheckpointHash !== current.checkpointHash) {
|
|
575
|
+
fail('LIVE_EVIDENCE_CONCURRENT_MODIFICATION', 'Connector checkpoint generation or digest changed.');
|
|
576
|
+
}
|
|
577
|
+
const target = CHECKPOINT_TRANSITIONS[current.state]?.[event.type];
|
|
578
|
+
if (target === undefined) fail('LIVE_EVIDENCE_TRANSITION_INVALID', 'Illegal connector checkpoint transition.', { from: current.state, type: event.type });
|
|
579
|
+
const patch = event.patch ?? {};
|
|
580
|
+
for (const field of IMMUTABLE_CHECKPOINT_FIELDS) {
|
|
581
|
+
if (Object.hasOwn(patch, field) && !sameValue(patch[field], current[field])) fail('LIVE_EVIDENCE_BINDING_MISMATCH', `Connector ${field} cannot change.`);
|
|
582
|
+
}
|
|
583
|
+
if (current.state !== 'prepared') {
|
|
584
|
+
for (const field of ['consentRecordId', 'consentRecordHash', 'consentExpiresAt', 'runtimeCapabilityHash']) {
|
|
585
|
+
if (Object.hasOwn(patch, field) && !sameValue(patch[field], current[field])) {
|
|
586
|
+
fail('LIVE_EVIDENCE_BINDING_MISMATCH', `Connector ${field} is frozen after request issuance.`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (event.type === 'request' && (patch.consentRecordId === null || patch.consentRecordHash === null
|
|
591
|
+
|| patch.consentExpiresAt === null || patch.runtimeCapabilityHash === null)) {
|
|
592
|
+
fail('LIVE_EVIDENCE_AUTHORITY_INVALID', 'Requesting a provider call requires current consent and one opaque runtime capability.');
|
|
593
|
+
}
|
|
594
|
+
if (event.type === 'materialize' && (patch.artifactId === null || patch.artifactHash === null
|
|
595
|
+
|| patch.health?.status !== 'available')) {
|
|
596
|
+
fail('LIVE_EVIDENCE_FALSE_PASS', 'Materialize transition requires exact Artifact and current available-health proof.');
|
|
597
|
+
}
|
|
598
|
+
if (['absent', 'uncertain'].includes(event.type) && patch.absenceDigest === null) {
|
|
599
|
+
fail('LIVE_EVIDENCE_CHECKPOINT_INVALID', 'Absent or uncertain transition requires a typed absence digest.');
|
|
600
|
+
}
|
|
601
|
+
const next = { ...clone(current), ...patch, state: target, generation: current.generation + 1, lastEventId: event.eventId, lastEventHash: eventHash, predecessorDigest: current.checkpointHash, updatedAt: event.at };
|
|
602
|
+
delete next.checkpointHash;
|
|
603
|
+
next.checkpointHash = sha256Jcs(next);
|
|
604
|
+
return assertOperatingConnectorCheckpointV2(next);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export function assertOperatingMeasurementPlanV2(plan, { verificationPlan } = {}) {
|
|
608
|
+
protocol('operating-measurement-plan', plan);
|
|
609
|
+
safePortable(plan);
|
|
610
|
+
selfHash(plan, 'planHash');
|
|
611
|
+
const terminal = [...plan.terminalResults].sort();
|
|
612
|
+
if (!sameValue(terminal, ['failed', 'insufficient-evidence', 'passed'])) fail('LIVE_EVIDENCE_PLAN_INVALID', 'Measurement plan must declare every terminal result.');
|
|
613
|
+
if (plan.target.unit !== plan.metric.unit || plan.baseline.unit !== plan.metric.unit) fail('LIVE_EVIDENCE_UNIT_MISMATCH', 'Baseline and target units must match the metric.');
|
|
614
|
+
if (![plan.baseline.value, ...(Array.isArray(plan.target.value) ? plan.target.value : [plan.target.value])].every(Number.isFinite)
|
|
615
|
+
|| Date.parse(plan.window.from) > Date.parse(plan.window.to) || Date.parse(plan.dueAt) < Date.parse(plan.window.to)) {
|
|
616
|
+
fail('LIVE_EVIDENCE_PLAN_INVALID', 'Measurement values must be finite and the inclusive window/due chronology must be ordered.');
|
|
617
|
+
}
|
|
618
|
+
const algebra = {
|
|
619
|
+
increase: ['gt', 'gte'], decrease: ['lt', 'lte'], maintain: ['eq'], range: ['between'],
|
|
620
|
+
};
|
|
621
|
+
if (!algebra[plan.target.direction]?.includes(plan.target.operator)) {
|
|
622
|
+
fail('LIVE_EVIDENCE_PLAN_INVALID', 'Measurement direction and operator are incompatible.');
|
|
623
|
+
}
|
|
624
|
+
if (plan.target.operator === 'between') {
|
|
625
|
+
if (!Array.isArray(plan.target.value) || plan.target.value.length !== 2 || plan.target.value[0] > plan.target.value[1]) {
|
|
626
|
+
fail('LIVE_EVIDENCE_PLAN_INVALID', 'Between target must be one ordered finite range.');
|
|
627
|
+
}
|
|
628
|
+
} else if (Array.isArray(plan.target.value)) {
|
|
629
|
+
fail('LIVE_EVIDENCE_PLAN_INVALID', 'Scalar measurement operators cannot use a range target.');
|
|
630
|
+
}
|
|
631
|
+
if ((plan.target.direction === 'increase' && plan.target.value <= plan.baseline.value)
|
|
632
|
+
|| (plan.target.direction === 'decrease' && plan.target.value >= plan.baseline.value)
|
|
633
|
+
|| (plan.target.direction === 'maintain' && plan.target.value !== plan.baseline.value)) {
|
|
634
|
+
fail('LIVE_EVIDENCE_PLAN_INVALID', 'Measurement target does not match the declared baseline direction.');
|
|
635
|
+
}
|
|
636
|
+
if (verificationPlan !== undefined) {
|
|
637
|
+
protocol('operating-action-verification-plan', verificationPlan);
|
|
638
|
+
exactDigest(verificationPlan, plan.verificationPlan.recordDigest, 'Action verification plan');
|
|
639
|
+
same([verificationPlan.verificationPlanId, verificationPlan.actionId, verificationPlan.metricId, verificationPlan.baseline, verificationPlan.target], [plan.verificationPlan.verificationPlanId, plan.action.actionId, plan.metric.metricId, plan.baseline.value, plan.verificationPlan.canonicalTarget], 'Measurement-plan specialization');
|
|
640
|
+
same(sha256Jcs(verificationPlan.window), plan.verificationPlan.windowDigest, 'Verification window digest');
|
|
641
|
+
same(sha256Jcs(verificationPlan.method), plan.verificationPlan.methodDigest, 'Verification method digest');
|
|
642
|
+
same(sha256Jcs(verificationPlan.evaluationRules), plan.verificationPlan.evaluationRulesDigest, 'Verification rule digest');
|
|
643
|
+
const targetValues = Array.isArray(plan.target.value) ? plan.target.value : [plan.target.value];
|
|
644
|
+
if (!targetValues.includes(verificationPlan.target)) fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Measurement target is not derivable from the canonical verification target.');
|
|
645
|
+
}
|
|
646
|
+
return freeze(clone(plan));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export function assertOperatingMeasurementScheduleV2(schedule) {
|
|
650
|
+
protocol('operating-measurement-schedule', schedule);
|
|
651
|
+
safePortable(schedule);
|
|
652
|
+
selfHash(schedule, 'scheduleHash');
|
|
653
|
+
if (schedule.generation === 0 && (schedule.state !== 'disabled' || schedule.runCount !== 0
|
|
654
|
+
|| schedule.nextEventIdentity !== null || schedule.nextDueAt !== null
|
|
655
|
+
|| schedule.lastReceiptId !== null || schedule.lastTransitionHash !== null)) {
|
|
656
|
+
fail('LIVE_EVIDENCE_SCHEDULE_INVALID', 'New schedules must be disabled and unadvanced.');
|
|
657
|
+
}
|
|
658
|
+
if (schedule.recurrence.kind === 'once' && schedule.recurrence.intervalSeconds !== null) fail('LIVE_EVIDENCE_SCHEDULE_INVALID', 'One-time schedules cannot carry an interval.');
|
|
659
|
+
if (schedule.recurrence.kind === 'interval' && schedule.recurrence.intervalSeconds === null) fail('LIVE_EVIDENCE_SCHEDULE_INVALID', 'Interval schedules require a bounded interval.');
|
|
660
|
+
if (Date.parse(schedule.startsAt) > Date.parse(schedule.endsAt ?? schedule.startsAt)
|
|
661
|
+
|| schedule.runCount > schedule.maxRuns) fail('LIVE_EVIDENCE_SCHEDULE_INVALID', 'Schedule time bounds or run count are invalid.');
|
|
662
|
+
if (schedule.state === 'enabled' && (schedule.consentRecordHashes.length === 0
|
|
663
|
+
|| schedule.nextEventIdentity === null || schedule.nextDueAt === null
|
|
664
|
+
|| schedule.runCount >= schedule.maxRuns || Date.parse(schedule.nextDueAt) < Date.parse(schedule.startsAt)
|
|
665
|
+
|| (schedule.endsAt !== null && Date.parse(schedule.nextDueAt) > Date.parse(schedule.endsAt)))) {
|
|
666
|
+
fail('LIVE_EVIDENCE_SCHEDULE_INVALID', 'Enabled schedule requires current consent, one due identity, and remaining bounded runs.');
|
|
667
|
+
}
|
|
668
|
+
if (['disabled', 'revoked', 'expired', 'completed'].includes(schedule.state)
|
|
669
|
+
&& (schedule.nextEventIdentity !== null || schedule.nextDueAt !== null)) {
|
|
670
|
+
fail('LIVE_EVIDENCE_SCHEDULE_INVALID', 'Inactive schedule cannot retain an executable due identity.');
|
|
671
|
+
}
|
|
672
|
+
return freeze(clone(schedule));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function assertOperatingMeasurementScheduleReceiptStructureV2(receipt) {
|
|
676
|
+
protocol('operating-measurement-schedule-receipt', receipt);
|
|
677
|
+
safePortable(receipt);
|
|
678
|
+
selfHash(receipt, 'receiptHash');
|
|
679
|
+
if (receipt.authority !== 'none' || receipt.scheduleHash !== receipt.resultingScheduleHash) fail('LIVE_EVIDENCE_SCHEDULE_INVALID', 'Schedule receipt is non-authoritative and must bind its exact result schedule.');
|
|
680
|
+
return freeze(clone(receipt));
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function assertScheduleReceiptPredecessor(receipt, previousSchedule) {
|
|
684
|
+
const previous = assertOperatingMeasurementScheduleV2(previousSchedule);
|
|
685
|
+
if (previous.scheduleHash !== receipt.previousScheduleHash || previous.scheduleId !== receipt.scheduleId
|
|
686
|
+
|| previous.state !== receipt.fromState || previous.runCount !== receipt.previousRunCount
|
|
687
|
+
|| previous.nextEventIdentity !== receipt.previousNextEventIdentity || previous.nextDueAt !== receipt.previousNextDueAt) {
|
|
688
|
+
fail('LIVE_EVIDENCE_BASE_RECORD_MISMATCH', 'Schedule receipt changed its exact predecessor.');
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function assertScheduleReceiptResult(receipt, resultingSchedule) {
|
|
693
|
+
const result = assertOperatingMeasurementScheduleV2(resultingSchedule);
|
|
694
|
+
if (result.scheduleHash !== receipt.resultingScheduleHash || result.scheduleId !== receipt.scheduleId
|
|
695
|
+
|| result.state !== receipt.toState || result.runCount !== receipt.resultingRunCount
|
|
696
|
+
|| result.nextEventIdentity !== receipt.resultingNextEventIdentity || result.nextDueAt !== receipt.resultingNextDueAt) {
|
|
697
|
+
fail('LIVE_EVIDENCE_BASE_RECORD_MISMATCH', 'Schedule receipt changed its exact result.');
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export function assertOperatingMeasurementScheduleReceiptV2(receipt, { previousSchedule, resultingSchedule } = {}) {
|
|
702
|
+
const normalized = assertOperatingMeasurementScheduleReceiptStructureV2(receipt);
|
|
703
|
+
if (previousSchedule === undefined || resultingSchedule === undefined) {
|
|
704
|
+
fail('LIVE_EVIDENCE_BASE_RECORD_REQUIRED', 'A certifying schedule receipt requires its exact predecessor and resulting schedules.');
|
|
705
|
+
}
|
|
706
|
+
assertScheduleReceiptPredecessor(normalized, previousSchedule);
|
|
707
|
+
assertScheduleReceiptResult(normalized, resultingSchedule);
|
|
708
|
+
return normalized;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function scheduleReceipt(schedule, transition, next) {
|
|
712
|
+
const receipt = {
|
|
713
|
+
kind: 'operating-measurement-schedule-receipt', schemaVersion: '1.0.0', protocolVersion: '2.0.0', receiptId: transition.receiptId,
|
|
714
|
+
authority: 'none', scheduleId: schedule.scheduleId, scheduleHash: next.scheduleHash,
|
|
715
|
+
previousScheduleHash: transition.expectedScheduleHash, resultingScheduleHash: next.scheduleHash,
|
|
716
|
+
measurementPlanId: schedule.measurementPlanId, planHash: schedule.planHash,
|
|
717
|
+
consentRecordHashes: clone(schedule.consentRecordHashes), ceilingsHash: schedule.ceilingsHash, clockHash: transition.clockHash,
|
|
718
|
+
recurrenceHash: sha256Jcs(schedule.recurrence), startsAt: schedule.startsAt, endsAt: schedule.endsAt, maxRuns: schedule.maxRuns,
|
|
719
|
+
misfirePolicy: schedule.misfirePolicy, dstPolicy: schedule.dstPolicy, transition: transition.type,
|
|
720
|
+
fromState: transition.expectedState, toState: next.state,
|
|
721
|
+
previousRunCount: transition.expectedRunCount, resultingRunCount: next.runCount,
|
|
722
|
+
previousNextEventIdentity: transition.expectedNextEventIdentity, resultingNextEventIdentity: next.nextEventIdentity,
|
|
723
|
+
previousNextDueAt: transition.expectedNextDueAt, resultingNextDueAt: next.nextDueAt,
|
|
724
|
+
ownerActorId: schedule.ownerActorId, eventId: transition.eventId, nextEventIdentity: next.nextEventIdentity,
|
|
725
|
+
at: transition.at,
|
|
726
|
+
};
|
|
727
|
+
receipt.receiptHash = sha256Jcs(receipt);
|
|
728
|
+
return receipt;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export function reduceOperatingMeasurementScheduleV2(scheduleValue, transitionValue, { priorReceipt } = {}) {
|
|
732
|
+
const schedule = assertOperatingMeasurementScheduleV2(scheduleValue);
|
|
733
|
+
const transition = clone(transitionValue);
|
|
734
|
+
assertExactKeys(transition, SCHEDULE_TRANSITION_FIELDS, 'Measurement schedule transition');
|
|
735
|
+
const transitionHash = sha256Jcs(transition);
|
|
736
|
+
if (schedule.lastReceiptId === transition.receiptId) {
|
|
737
|
+
if (schedule.lastTransitionHash !== transitionHash) fail('LIVE_EVIDENCE_REPLAY_CONFLICT', 'Divergent schedule transition reused one receipt identity.');
|
|
738
|
+
if (priorReceipt === undefined) fail('LIVE_EVIDENCE_BASE_RECORD_REQUIRED', 'Exact schedule replay requires the stored immutable prior receipt.');
|
|
739
|
+
const receipt = assertOperatingMeasurementScheduleReceiptStructureV2(priorReceipt);
|
|
740
|
+
const expected = scheduleReceipt(schedule, transition, schedule);
|
|
741
|
+
if (!sameValue(receipt, expected)) fail('LIVE_EVIDENCE_REPLAY_CONFLICT', 'Stored schedule receipt does not match the exact replay request and resulting schedule.');
|
|
742
|
+
assertScheduleReceiptResult(receipt, schedule);
|
|
743
|
+
return freeze({ replay: true, schedule, receipt });
|
|
744
|
+
}
|
|
745
|
+
if (transition.expectedGeneration !== schedule.generation || transition.expectedScheduleHash !== schedule.scheduleHash) fail('LIVE_EVIDENCE_CONCURRENT_MODIFICATION', 'Schedule generation or digest changed.');
|
|
746
|
+
if (transition.expectedState !== schedule.state || transition.expectedRunCount !== schedule.runCount
|
|
747
|
+
|| transition.expectedNextEventIdentity !== schedule.nextEventIdentity || transition.expectedNextDueAt !== schedule.nextDueAt) {
|
|
748
|
+
fail('LIVE_EVIDENCE_CONCURRENT_MODIFICATION', 'Schedule state, run count, or due identity changed.');
|
|
749
|
+
}
|
|
750
|
+
let toState = SCHEDULE_TRANSITIONS[schedule.state]?.[transition.type];
|
|
751
|
+
if (toState === undefined) fail('LIVE_EVIDENCE_TRANSITION_INVALID', 'Illegal measurement schedule transition.', { from: schedule.state, type: transition.type });
|
|
752
|
+
if (transition.planHash !== schedule.planHash || !sameValue(transition.consentRecordHashes, schedule.consentRecordHashes)
|
|
753
|
+
|| transition.ceilingsHash !== schedule.ceilingsHash) fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Schedule transition changed plan or consent custody.');
|
|
754
|
+
let runCount = schedule.runCount;
|
|
755
|
+
if (transition.type === 'enable') {
|
|
756
|
+
if (schedule.consentRecordHashes.length === 0 || transition.nextEventIdentity === null || transition.nextDueAt === null
|
|
757
|
+
|| Date.parse(transition.nextDueAt) < Date.parse(schedule.startsAt)) {
|
|
758
|
+
fail('LIVE_EVIDENCE_AUTHORITY_INVALID', 'Enabling measurement requires consent custody and a bounded runtime-issued due identity.');
|
|
759
|
+
}
|
|
760
|
+
} else if (transition.type === 'run-issued') {
|
|
761
|
+
if (schedule.nextEventIdentity !== transition.eventId || schedule.nextDueAt === null
|
|
762
|
+
|| Date.parse(transition.at) < Date.parse(schedule.nextDueAt)
|
|
763
|
+
|| (schedule.endsAt !== null && Date.parse(transition.at) > Date.parse(schedule.endsAt))) {
|
|
764
|
+
fail('LIVE_EVIDENCE_TRANSITION_INVALID', 'Measurement run is not the exact due runtime identity within its schedule window.');
|
|
765
|
+
}
|
|
766
|
+
runCount += 1;
|
|
767
|
+
const terminalRun = runCount >= schedule.maxRuns || schedule.recurrence.kind === 'once';
|
|
768
|
+
toState = terminalRun ? 'completed' : 'enabled';
|
|
769
|
+
if (terminalRun ? (transition.nextEventIdentity !== null || transition.nextDueAt !== null)
|
|
770
|
+
: (transition.nextEventIdentity === null || transition.nextDueAt === null
|
|
771
|
+
|| transition.nextEventIdentity === transition.eventId
|
|
772
|
+
|| Date.parse(transition.nextDueAt) <= Date.parse(schedule.nextDueAt))) {
|
|
773
|
+
fail('LIVE_EVIDENCE_TRANSITION_INVALID', 'Measurement run successor identity or completion state is invalid.');
|
|
774
|
+
}
|
|
775
|
+
} else if (['disable', 'revoke', 'expire', 'complete'].includes(transition.type)) {
|
|
776
|
+
if (transition.nextEventIdentity !== null || transition.nextDueAt !== null) fail('LIVE_EVIDENCE_TRANSITION_INVALID', 'Inactive transition cannot retain an executable due identity.');
|
|
777
|
+
} else if (transition.nextEventIdentity !== schedule.nextEventIdentity || transition.nextDueAt !== schedule.nextDueAt) {
|
|
778
|
+
fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Pause or resume cannot substitute the scheduled due identity.');
|
|
779
|
+
}
|
|
780
|
+
const next = { ...clone(schedule), generation: schedule.generation + 1, state: toState, runCount,
|
|
781
|
+
nextEventIdentity: transition.nextEventIdentity, nextDueAt: transition.nextDueAt,
|
|
782
|
+
lastReceiptId: transition.receiptId, lastTransitionHash: transitionHash };
|
|
783
|
+
delete next.scheduleHash;
|
|
784
|
+
next.scheduleHash = sha256Jcs(next);
|
|
785
|
+
const normalizedNext = assertOperatingMeasurementScheduleV2(next);
|
|
786
|
+
const receipt = scheduleReceipt(schedule, transition, normalizedNext);
|
|
787
|
+
return freeze({ replay: false, schedule: normalizedNext,
|
|
788
|
+
receipt: assertOperatingMeasurementScheduleReceiptV2(receipt, { previousSchedule: schedule, resultingSchedule: normalizedNext }) });
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export function assertOperatingEvidenceObservationV2(observation, {
|
|
792
|
+
measurementPlan, metricObservation, ingestion, connectorCheckpoint, artifact, evidenceRefs, acceptedBridge,
|
|
793
|
+
} = {}) {
|
|
794
|
+
protocol('operating-evidence-observation', observation);
|
|
795
|
+
safePortable(observation);
|
|
796
|
+
selfHash(observation, 'observationHash');
|
|
797
|
+
if (measurementPlan === undefined) fail('LIVE_EVIDENCE_BASE_RECORD_REQUIRED', 'Evidence observation requires the exact measurement plan.');
|
|
798
|
+
const plan = assertOperatingMeasurementPlanV2(measurementPlan);
|
|
799
|
+
if (plan.measurementPlanId !== observation.measurementPlan.recordId || plan.planHash !== observation.measurementPlan.recordDigest
|
|
800
|
+
|| plan.metric.metricId !== observation.metricId || plan.metric.unit !== observation.unit || plan.metric.dimension !== observation.dimension) {
|
|
801
|
+
fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Evidence observation changed measurement-plan metric custody.');
|
|
802
|
+
}
|
|
803
|
+
if (ingestion === undefined || connectorCheckpoint === undefined || artifact === undefined || !Array.isArray(evidenceRefs)
|
|
804
|
+
|| acceptedBridge === undefined) {
|
|
805
|
+
fail('LIVE_EVIDENCE_BASE_RECORD_REQUIRED', 'Evidence observation requires the exact accepted ingestion bridge, checkpoint, Artifact, and EvidenceRef custody.');
|
|
806
|
+
}
|
|
807
|
+
const accepted = assertAcceptedLiveEvidenceBridgeV2(ingestion, acceptedBridge);
|
|
808
|
+
if (accepted.ingestionHash !== ingestion.ingestionHash
|
|
809
|
+
|| acceptedBridge.artifact?.artifactId !== artifact.artifactId
|
|
810
|
+
|| acceptedBridge.resultCheckpoint?.checkpointId !== connectorCheckpoint.checkpointId
|
|
811
|
+
|| evidenceRefs.length !== 1
|
|
812
|
+
|| acceptedBridge.evidenceRef?.evidenceRefId !== evidenceRefs[0]?.evidenceRefId
|
|
813
|
+
|| sha256Jcs(acceptedBridge.evidenceRef) !== sha256Jcs(evidenceRefs[0])) {
|
|
814
|
+
fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Observation custody is not the exact accepted live-evidence bridge.');
|
|
815
|
+
}
|
|
816
|
+
const checkpoint = assertOperatingConnectorCheckpointV2(connectorCheckpoint);
|
|
817
|
+
protocol('operating-artifact', artifact);
|
|
818
|
+
if (observation.ingestion.ingestionId !== ingestion.ingestionId || observation.ingestion.ingestionHash !== ingestion.ingestionHash
|
|
819
|
+
|| observation.checkpoint.checkpointId !== checkpoint.checkpointId || observation.checkpoint.checkpointHash !== checkpoint.checkpointHash
|
|
820
|
+
|| observation.artifact.artifactId !== artifact.artifactId || observation.artifact.recordDigest !== sha256Jcs(artifact)
|
|
821
|
+
|| artifact.canonicalHash !== sha256Jcs(ingestion) || ingestion.providerRegistrationHash !== observation.providerRegistrationHash
|
|
822
|
+
|| !sameValue(ingestion.provider, observation.provider)) {
|
|
823
|
+
fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Evidence observation changed live-ingestion Artifact or provider custody.');
|
|
824
|
+
}
|
|
825
|
+
const refs = evidenceRefs.map((entry) => {
|
|
826
|
+
protocol('operating-evidence-ref', entry);
|
|
827
|
+
return { evidenceRefId: entry.evidenceRefId, recordDigest: sha256Jcs(entry) };
|
|
828
|
+
}).sort((left, right) => left.evidenceRefId.localeCompare(right.evidenceRefId));
|
|
829
|
+
const claimedRefs = [...observation.evidenceRefs].sort((left, right) => left.evidenceRefId.localeCompare(right.evidenceRefId));
|
|
830
|
+
if (!sameValue(refs, claimedRefs)) fail('LIVE_EVIDENCE_BASE_RECORD_MISMATCH', 'Evidence observation changed EvidenceRef membership or bytes.');
|
|
831
|
+
if (observation.status === 'observed') {
|
|
832
|
+
currentAt(observation.observedAt, observation.freshUntil, 'Observation');
|
|
833
|
+
if (metricObservation === undefined) fail('LIVE_EVIDENCE_BASE_RECORD_REQUIRED', 'Observed evidence requires the exact canonical metric observation.');
|
|
834
|
+
protocol('operating-metric-observation', metricObservation);
|
|
835
|
+
exactDigest(metricObservation, observation.metricObservation.recordDigest, 'Metric observation');
|
|
836
|
+
same([metricObservation.observationId, metricObservation.metricId, metricObservation.value, metricObservation.unit, metricObservation.observedAt, metricObservation.sourceArtifactId, metricObservation.evidenceRefIds], [observation.metricObservation.recordId, observation.metricId, observation.value, observation.unit, observation.observedAt, observation.artifact.artifactId, observation.evidenceRefs.map((entry) => entry.evidenceRefId)], 'Metric observation');
|
|
837
|
+
} else if ((observation.status === 'absent' && observation.absences.length === 0)
|
|
838
|
+
|| (observation.status === 'contradicted' && observation.contradictions.length === 0)) {
|
|
839
|
+
fail('LIVE_EVIDENCE_FALSE_PASS', 'Non-observed evidence must retain its typed absence or contradiction.');
|
|
840
|
+
}
|
|
841
|
+
return freeze(clone(observation));
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function observationCustodyFor(observation, custody) {
|
|
845
|
+
const matches = custody.filter((entry) => entry?.evidenceObservationId === observation.evidenceObservationId);
|
|
846
|
+
if (matches.length !== 1) fail('LIVE_EVIDENCE_BASE_RECORD_REQUIRED', 'Each evidence observation requires one exact custody bundle.', { evidenceObservationId: observation.evidenceObservationId });
|
|
847
|
+
return matches[0];
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function canonicalEvaluationInputs({ evaluation, plan, evidenceObservations, metricObservations, observationCustody }) {
|
|
851
|
+
if (!Array.isArray(observationCustody)) fail('LIVE_EVIDENCE_BASE_RECORD_REQUIRED', 'Outcome evaluation requires exact observation custody bundles.');
|
|
852
|
+
const ids = new Set();
|
|
853
|
+
const normalizedEvidence = evidenceObservations.map((entry) => {
|
|
854
|
+
if (ids.has(entry.evidenceObservationId)) fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Duplicate evidence observation identity is forbidden.', { evidenceObservationId: entry.evidenceObservationId });
|
|
855
|
+
ids.add(entry.evidenceObservationId);
|
|
856
|
+
const custody = observationCustodyFor(entry, observationCustody);
|
|
857
|
+
const metricObservation = entry.metricObservation === null ? undefined
|
|
858
|
+
: metricObservations.find((candidate) => candidate.observationId === entry.metricObservation.recordId);
|
|
859
|
+
return assertOperatingEvidenceObservationV2(entry, { measurementPlan: plan, metricObservation, ...custody });
|
|
860
|
+
}).sort((left, right) => left.evidenceObservationId.localeCompare(right.evidenceObservationId));
|
|
861
|
+
if (normalizedEvidence.length !== evaluation.observations.length || observationCustody.length !== normalizedEvidence.length) {
|
|
862
|
+
fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Evaluation observation membership changed.');
|
|
863
|
+
}
|
|
864
|
+
const wrappers = normalizedEvidence.map((entry) => ({
|
|
865
|
+
evidenceObservationId: entry.evidenceObservationId,
|
|
866
|
+
observationId: entry.metricObservation?.recordId ?? null,
|
|
867
|
+
observationHash: entry.observationHash,
|
|
868
|
+
metricObservationDigest: entry.metricObservation?.recordDigest ?? null,
|
|
869
|
+
value: entry.value,
|
|
870
|
+
unit: entry.unit,
|
|
871
|
+
observedAt: entry.observedAt,
|
|
872
|
+
freshUntil: entry.freshUntil,
|
|
873
|
+
status: entry.status,
|
|
874
|
+
}));
|
|
875
|
+
if (!sameValue(evaluation.observations, wrappers)) fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Evaluation wrappers are not the canonical unique observation set.');
|
|
876
|
+
const inWindow = normalizedEvidence.filter((entry) => Date.parse(entry.observedAt) >= Date.parse(plan.window.from)
|
|
877
|
+
&& Date.parse(entry.observedAt) <= Date.parse(plan.window.to));
|
|
878
|
+
const observed = inWindow.filter((entry) => entry.status === 'observed' && Date.parse(entry.freshUntil) > Date.parse(evaluation.evaluatedAt));
|
|
879
|
+
const absences = inWindow.flatMap((entry) => entry.absences.map((absence) => absence.detailsHash));
|
|
880
|
+
for (const entry of inWindow.filter((item) => item.status === 'observed' && Date.parse(item.freshUntil) <= Date.parse(evaluation.evaluatedAt))) {
|
|
881
|
+
absences.push(sha256Jcs({ kind: 'stale', evidenceObservationId: entry.evidenceObservationId, observationHash: entry.observationHash, evaluatedAt: evaluation.evaluatedAt }));
|
|
882
|
+
}
|
|
883
|
+
const contradictions = inWindow.flatMap((entry) => entry.contradictions);
|
|
884
|
+
return {
|
|
885
|
+
normalizedEvidence, wrappers, inWindow, observed,
|
|
886
|
+
absences: [...new Set(absences)].sort(), contradictions: [...new Set(contradictions)].sort(),
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
export function assertOperatingOutcomeEvaluationV2(evaluation, {
|
|
891
|
+
measurementPlan, verificationPlan, outcome, evidenceObservations, metricObservations = [],
|
|
892
|
+
observationCustody, previousEvaluation,
|
|
893
|
+
} = {}) {
|
|
894
|
+
protocol('operating-outcome-evaluation', evaluation);
|
|
895
|
+
safePortable(evaluation);
|
|
896
|
+
selfHash(evaluation, 'evaluationHash');
|
|
897
|
+
if (measurementPlan === undefined || verificationPlan === undefined || outcome === undefined || !Array.isArray(evidenceObservations)) fail('LIVE_EVIDENCE_BASE_RECORD_REQUIRED', 'Outcome evaluation requires its exact plan, Outcome, and evidence observations.');
|
|
898
|
+
const plan = assertOperatingMeasurementPlanV2(measurementPlan, { verificationPlan });
|
|
899
|
+
if (plan.planHash !== evaluation.measurementPlan.recordDigest || plan.measurementPlanId !== evaluation.measurementPlan.recordId) fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Evaluation changed its measurement plan.');
|
|
900
|
+
const canonical = canonicalEvaluationInputs({ evaluation, plan, evidenceObservations, metricObservations, observationCustody });
|
|
901
|
+
if (!sameValue([evaluation.window, evaluation.operator, evaluation.unit, evaluation.dimension, evaluation.baseline, evaluation.target, evaluation.minimumEvidence],
|
|
902
|
+
[plan.window, plan.target.operator, plan.metric.unit, plan.metric.dimension, plan.baseline.value, plan.target.value, plan.minimumEvidence])) {
|
|
903
|
+
fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Evaluation changed its exact plan algebra, window, or threshold.');
|
|
904
|
+
}
|
|
905
|
+
const computedResult = canonical.absences.length > 0 || canonical.contradictions.length > 0 || canonical.observed.length < plan.minimumEvidence
|
|
906
|
+
? 'insufficient-evidence' : canonical.observed.every((entry) => compare(plan.target.operator, entry.value, plan.target.value)) ? 'passed' : 'failed';
|
|
907
|
+
const computedConfidence = canonical.inWindow.length === 0 ? 0 : canonical.observed.length / Math.max(canonical.inWindow.length, plan.minimumEvidence);
|
|
908
|
+
const computedInputDigest = sha256Jcs(canonical.wrappers.map((entry) => entry.observationHash));
|
|
909
|
+
const computedProviders = [...new Map(canonical.inWindow.map((entry) => [`${entry.provider.id}@${entry.provider.version}`, entry.provider])).values()].sort((a, b) => `${a.id}@${a.version}`.localeCompare(`${b.id}@${b.version}`));
|
|
910
|
+
if (evaluation.result !== computedResult || evaluation.confidence !== computedConfidence || evaluation.exactInputDigest !== computedInputDigest
|
|
911
|
+
|| !sameValue(evaluation.providerVersions, computedProviders) || !sameValue(evaluation.absences, canonical.absences)
|
|
912
|
+
|| !sameValue(evaluation.contradictions, canonical.contradictions)) fail('LIVE_EVIDENCE_FALSE_PASS', 'Outcome result, confidence, input digest, provider, absence, or contradiction truth is not deterministic from bound evidence.');
|
|
913
|
+
if (verificationPlan !== undefined) {
|
|
914
|
+
protocol('operating-action-verification-plan', verificationPlan);
|
|
915
|
+
exactDigest(verificationPlan, evaluation.verificationPlan.recordDigest, 'Verification plan');
|
|
916
|
+
same([verificationPlan.verificationPlanId, verificationPlan.actionId, verificationPlan.metricId], [evaluation.verificationPlan.recordId, evaluation.actionId, evaluation.metricId], 'Evaluation verification plan');
|
|
917
|
+
}
|
|
918
|
+
if (outcome !== undefined) {
|
|
919
|
+
protocol('operating-outcome', outcome);
|
|
920
|
+
exactDigest(outcome, evaluation.outcome.recordDigest, 'Outcome');
|
|
921
|
+
const status = evaluation.result === 'passed' ? 'succeeded' : evaluation.result === 'failed' ? 'failed' : 'insufficient-evidence';
|
|
922
|
+
same([outcome.outcomeId, outcome.actionId, outcome.verificationPlanId, outcome.status], [evaluation.outcome.recordId, evaluation.actionId, evaluation.verificationPlan.recordId, status], 'Outcome evaluation');
|
|
923
|
+
}
|
|
924
|
+
if (evaluation.previousEvaluationHash === null) {
|
|
925
|
+
if (previousEvaluation !== undefined && previousEvaluation !== null) fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Initial evaluation cannot carry a predecessor.');
|
|
926
|
+
} else {
|
|
927
|
+
if (previousEvaluation === undefined || previousEvaluation === null) fail('LIVE_EVIDENCE_BASE_RECORD_REQUIRED', 'Reevaluation requires the exact immutable predecessor.');
|
|
928
|
+
protocol('operating-outcome-evaluation', previousEvaluation);
|
|
929
|
+
selfHash(previousEvaluation, 'evaluationHash');
|
|
930
|
+
if (previousEvaluation.evaluationHash !== evaluation.previousEvaluationHash
|
|
931
|
+
|| !sameValue([previousEvaluation.measurementPlan, previousEvaluation.verificationPlan, previousEvaluation.outcome],
|
|
932
|
+
[evaluation.measurementPlan, evaluation.verificationPlan, evaluation.outcome])
|
|
933
|
+
|| Date.parse(previousEvaluation.evaluatedAt) >= Date.parse(evaluation.evaluatedAt)
|
|
934
|
+
|| previousEvaluation.exactInputDigest === evaluation.exactInputDigest) {
|
|
935
|
+
fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Reevaluation predecessor, lineage, time, or changed evidence custody is invalid.');
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (evaluation.revisit.required !== (evaluation.result !== 'passed')
|
|
939
|
+
|| !sameValue(evaluation.revisit.decisionIds, verificationPlan.revisitDecisionIds)) {
|
|
940
|
+
fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Evaluation revisit truth changed from its result or verification plan.');
|
|
941
|
+
}
|
|
942
|
+
return freeze(clone(evaluation));
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function compare(operator, value, target) {
|
|
946
|
+
if (operator === 'gt') return value > target;
|
|
947
|
+
if (operator === 'gte') return value >= target;
|
|
948
|
+
if (operator === 'lt') return value < target;
|
|
949
|
+
if (operator === 'lte') return value <= target;
|
|
950
|
+
if (operator === 'eq') return value === target;
|
|
951
|
+
return value >= target[0] && value <= target[1];
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
export function evaluateOperatingOutcomeV2({
|
|
955
|
+
plan, observations, metricObservations, observationCustody, evaluationId, outcome, verificationPlan,
|
|
956
|
+
evaluatedAt, previousEvaluation = null,
|
|
957
|
+
}) {
|
|
958
|
+
const normalizedPlan = assertOperatingMeasurementPlanV2(plan, { verificationPlan });
|
|
959
|
+
const normalized = observations.map((entry) => {
|
|
960
|
+
const custody = observationCustodyFor(entry, observationCustody);
|
|
961
|
+
return assertOperatingEvidenceObservationV2(entry, { measurementPlan: normalizedPlan,
|
|
962
|
+
metricObservation: entry.metricObservation === null ? undefined : metricObservations.find((candidate) => candidate.observationId === entry.metricObservation.recordId),
|
|
963
|
+
...custody });
|
|
964
|
+
}).sort((left, right) => left.evidenceObservationId.localeCompare(right.evidenceObservationId));
|
|
965
|
+
const inWindow = normalized.filter((entry) => Date.parse(entry.observedAt) >= Date.parse(plan.window.from) && Date.parse(entry.observedAt) <= Date.parse(plan.window.to));
|
|
966
|
+
const observed = inWindow.filter((entry) => entry.status === 'observed' && Date.parse(entry.freshUntil) > Date.parse(evaluatedAt));
|
|
967
|
+
const absences = inWindow.flatMap((entry) => entry.absences.map((absence) => absence.detailsHash));
|
|
968
|
+
for (const entry of inWindow.filter((item) => item.status === 'observed' && Date.parse(item.freshUntil) <= Date.parse(evaluatedAt))) {
|
|
969
|
+
absences.push(sha256Jcs({ kind: 'stale', evidenceObservationId: entry.evidenceObservationId, observationHash: entry.observationHash, evaluatedAt }));
|
|
970
|
+
}
|
|
971
|
+
const contradictions = inWindow.flatMap((entry) => entry.contradictions);
|
|
972
|
+
let result = 'insufficient-evidence';
|
|
973
|
+
if (absences.length === 0 && contradictions.length === 0 && observed.length >= plan.minimumEvidence) {
|
|
974
|
+
result = observed.every((entry) => compare(plan.target.operator, entry.value, plan.target.value)) ? 'passed' : 'failed';
|
|
975
|
+
}
|
|
976
|
+
const evaluation = {
|
|
977
|
+
kind: 'operating-outcome-evaluation', schemaVersion: '1.0.0', protocolVersion: '2.0.0', evaluationId, previousEvaluationHash: previousEvaluation?.evaluationHash ?? null,
|
|
978
|
+
measurementPlan: { contractId: 'operating-measurement-plan', schemaVersion: '1.0.0', protocolVersion: '2.0.0', recordId: plan.measurementPlanId, recordDigest: plan.planHash },
|
|
979
|
+
verificationPlan: { contractId: 'operating-action-verification-plan', schemaVersion: '1.0.0', protocolVersion: '2.0.0', recordId: verificationPlan.verificationPlanId, recordDigest: sha256Jcs(verificationPlan) },
|
|
980
|
+
outcome: { contractId: 'operating-outcome', schemaVersion: '1.0.0', protocolVersion: '2.0.0', recordId: outcome.outcomeId, recordDigest: sha256Jcs(outcome) },
|
|
981
|
+
actionId: plan.action.actionId, metricId: plan.metric.metricId,
|
|
982
|
+
observations: normalized.map((entry) => ({ evidenceObservationId: entry.evidenceObservationId, observationId: entry.metricObservation?.recordId ?? null, observationHash: entry.observationHash, metricObservationDigest: entry.metricObservation?.recordDigest ?? null, value: entry.value, unit: entry.unit, observedAt: entry.observedAt, freshUntil: entry.freshUntil, status: entry.status })),
|
|
983
|
+
providerVersions: [...new Map(inWindow.map((entry) => [`${entry.provider.id}@${entry.provider.version}`, entry.provider])).values()].sort((a, b) => `${a.id}@${a.version}`.localeCompare(`${b.id}@${b.version}`)),
|
|
984
|
+
window: clone(plan.window), operator: plan.target.operator, unit: plan.metric.unit, dimension: plan.metric.dimension,
|
|
985
|
+
baseline: plan.baseline.value, target: clone(plan.target.value), minimumEvidence: plan.minimumEvidence,
|
|
986
|
+
exactInputDigest: sha256Jcs(normalized.map((entry) => entry.observationHash)), result,
|
|
987
|
+
confidence: inWindow.length === 0 ? 0 : observed.length / Math.max(inWindow.length, plan.minimumEvidence), absences: [...new Set(absences)].sort(),
|
|
988
|
+
contradictions: [...new Set(contradictions)].sort(), evaluatedAt,
|
|
989
|
+
revisit: { required: result !== 'passed', condition: result === 'passed' ? null : 'Evidence did not prove the declared target.', decisionIds: clone(verificationPlan.revisitDecisionIds) },
|
|
990
|
+
};
|
|
991
|
+
evaluation.evaluationHash = sha256Jcs(evaluation);
|
|
992
|
+
return assertOperatingOutcomeEvaluationV2(evaluation, { measurementPlan: normalizedPlan, verificationPlan, outcome,
|
|
993
|
+
evidenceObservations: normalized, metricObservations, observationCustody, previousEvaluation });
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
export function assertOperatingLearningReceiptV2(receipt, {
|
|
997
|
+
learning, outcome, evaluation, measurementPlan, verificationPlan, evidenceObservations,
|
|
998
|
+
metricObservations, observationCustody, previousEvaluation,
|
|
999
|
+
} = {}) {
|
|
1000
|
+
protocol('operating-learning-receipt', receipt);
|
|
1001
|
+
safePortable(receipt);
|
|
1002
|
+
selfHash(receipt, 'receiptHash');
|
|
1003
|
+
if (learning === undefined || outcome === undefined || evaluation === undefined
|
|
1004
|
+
|| measurementPlan === undefined || verificationPlan === undefined
|
|
1005
|
+
|| !Array.isArray(evidenceObservations) || !Array.isArray(metricObservations)
|
|
1006
|
+
|| !Array.isArray(observationCustody)) {
|
|
1007
|
+
fail('LIVE_EVIDENCE_BASE_RECORD_REQUIRED', 'A certifying Learning receipt requires exact Learning, Outcome, evaluation, plan, and observation custody.');
|
|
1008
|
+
}
|
|
1009
|
+
protocol('operating-learning', learning);
|
|
1010
|
+
exactDigest(learning, receipt.learning.recordDigest, 'Learning');
|
|
1011
|
+
same([learning.learningId, learning.outcomeId, learning.assumptionIds, learning.decisionIds, learning.evidenceRefIds], [receipt.learning.recordId, receipt.outcome.recordId, receipt.assumptionIds, receipt.decisionIds, receipt.evidenceRefIds], 'Learning receipt');
|
|
1012
|
+
same(sha256Jcs(learning.statement), receipt.statementDigest, 'Learning statement digest');
|
|
1013
|
+
protocol('operating-outcome', outcome);
|
|
1014
|
+
exactDigest(outcome, receipt.outcome.recordDigest, 'Learning outcome');
|
|
1015
|
+
assertOperatingOutcomeEvaluationV2(evaluation, { measurementPlan, verificationPlan, outcome,
|
|
1016
|
+
evidenceObservations, metricObservations, observationCustody, previousEvaluation });
|
|
1017
|
+
if (evaluation.evaluationId !== receipt.evaluation.evaluationId || evaluation.evaluationHash !== receipt.evaluation.evaluationHash || evaluation.result !== receipt.evaluation.result) fail('LIVE_EVIDENCE_BINDING_MISMATCH', 'Learning receipt changed the evaluation result.');
|
|
1018
|
+
return freeze(clone(receipt));
|
|
1019
|
+
}
|