@echopath-labs/forgerail 0.1.0-alpha.1 → 0.1.0-alpha.2
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/.codex-plugin/plugin.json +7 -3
- package/CHANGELOG.md +8 -0
- package/PLUGIN.md +2 -1
- package/PRIVACY.md +15 -0
- package/README.md +14 -2
- package/README.zh-CN.md +13 -2
- package/TERMS.md +19 -0
- package/adapters/codex.json +1 -1
- package/assets/forgerail-icon.svg +6 -0
- package/assets/forgerail-logo.svg +7 -0
- package/contracts/authority-evidence-v1.schema.json +48 -0
- package/contracts/cross-workspace-pack-composition-v1.schema.json +241 -0
- package/contracts/effective-profile-v2.schema.json +113 -0
- package/contracts/entry-mode-v1.schema.json +20 -0
- package/contracts/evidence-identity-v1.schema.json +48 -0
- package/contracts/execution-context-identity-v1.schema.json +76 -0
- package/contracts/gate-result-v1.schema.json +32 -0
- package/contracts/governance-source-v1.schema.json +30 -0
- package/contracts/host-adapter-observation-v1.schema.json +61 -0
- package/contracts/limited-reason-v1.schema.json +46 -0
- package/contracts/operation-authority-requirement-v1.schema.json +43 -0
- package/contracts/operation-grant-v1.schema.json +83 -0
- package/contracts/phase-slice-correlation-v1.schema.json +20 -0
- package/contracts/profile-explanation-v1.schema.json +91 -0
- package/contracts/provider-adapter-observation-v1.schema.json +87 -0
- package/contracts/return-receipt-v2.schema.json +55 -0
- package/contracts/review-authority-requirement-v1.schema.json +117 -0
- package/contracts/rollback-envelope-lineage-v1.schema.json +46 -0
- package/contracts/rule-claim-v1.schema.json +34 -0
- package/contracts/source-dependency-edge-v1.schema.json +37 -0
- package/contracts/task-control-revision-v1.schema.json +39 -0
- package/contracts/task-envelope-v2.schema.json +67 -0
- package/contracts/validation-result-v1.schema.json +57 -0
- package/contracts/validation-topology-v1.schema.json +55 -0
- package/contracts/workspace-identity-v1.schema.json +28 -0
- package/contracts/workspace-relationship-v1.schema.json +31 -0
- package/directory/README.md +23 -0
- package/directory/evaluations.json +78 -0
- package/directory/release-notes-alpha2.md +7 -0
- package/directory/submission-candidate.json +80 -0
- package/docs/composable-autonomy.zh-CN.md +183 -0
- package/docs/control-authority-validation-contracts.md +36 -0
- package/docs/control-authority-validation-contracts.zh-CN.md +36 -0
- package/docs/control-profile-contracts.md +35 -0
- package/docs/control-profile-contracts.zh-CN.md +35 -0
- package/docs/control-system-fixture-matrix.md +30 -0
- package/docs/control-system-fixture-matrix.zh-CN.md +30 -0
- package/docs/control-task-contracts.md +39 -0
- package/docs/control-task-contracts.zh-CN.md +39 -0
- package/docs/cross-workspace-pack-composition-contract.md +31 -0
- package/docs/cross-workspace-pack-composition-contract.zh-CN.md +31 -0
- package/docs/installation.md +9 -3
- package/docs/installation.zh-CN.md +9 -3
- package/docs/release-alpha2.md +21 -0
- package/docs/release-alpha2.zh-CN.md +21 -0
- package/package.json +9 -3
- package/packs/architecture-convergence-audit.json +30 -0
- package/scripts/build-universal-directory-candidate.mjs +72 -0
- package/scripts/disposable-consumer.mjs +1 -1
- package/scripts/fixtures/contracts/authority-evidence.revoked-without-invalidation.invalid.json +22 -0
- package/scripts/fixtures/contracts/authority-evidence.revoked.valid.json +32 -0
- package/scripts/fixtures/contracts/authority-evidence.valid.json +22 -0
- package/scripts/fixtures/contracts/capability-pack.missing-entry.invalid.json +14 -0
- package/scripts/fixtures/contracts/capability-pack.valid.json +15 -0
- package/scripts/fixtures/contracts/cross-workspace-pack-composition.authority-collapse.invalid.json +156 -0
- package/scripts/fixtures/contracts/cross-workspace-pack-composition.false-closure.invalid.json +138 -0
- package/scripts/fixtures/contracts/cross-workspace-pack-composition.valid.json +156 -0
- package/scripts/fixtures/contracts/effective-profile-v2.optional-unavailable-degraded.valid.json +36 -0
- package/scripts/fixtures/contracts/effective-profile-v2.required-unavailable-degraded.invalid.json +36 -0
- package/scripts/fixtures/contracts/effective-profile-v2.unresolved-claim-false-complete.invalid.json +34 -0
- package/scripts/fixtures/contracts/effective-profile-v2.valid.json +53 -0
- package/scripts/fixtures/contracts/effective-profile.duplicate-rule.invalid.json +23 -0
- package/scripts/fixtures/contracts/entry-mode.missing-evidence.invalid.json +6 -0
- package/scripts/fixtures/contracts/entry-mode.valid.json +6 -0
- package/scripts/fixtures/contracts/evidence-identity.unavailable-without-reason.invalid.json +21 -0
- package/scripts/fixtures/contracts/evidence-identity.valid.json +21 -0
- package/scripts/fixtures/contracts/execution-context-identity.unavailable-without-reason.invalid.json +31 -0
- package/scripts/fixtures/contracts/execution-context-identity.valid.json +31 -0
- package/scripts/fixtures/contracts/gate-result.satisfied-without-evidence.invalid.json +14 -0
- package/scripts/fixtures/contracts/gate-result.valid.json +14 -0
- package/scripts/fixtures/contracts/governance-source.unavailable-without-reason.invalid.json +16 -0
- package/scripts/fixtures/contracts/governance-source.valid.json +16 -0
- package/scripts/fixtures/contracts/host-adapter-observation.claims-authorization.invalid.json +23 -0
- package/scripts/fixtures/contracts/host-adapter-observation.valid.json +39 -0
- package/scripts/fixtures/contracts/host-adapter.codex.valid.json +1 -1
- package/scripts/fixtures/contracts/host-adapter.false-supported.invalid.json +14 -0
- package/scripts/fixtures/contracts/host-binding-receipt.valid.json +1 -1
- package/scripts/fixtures/contracts/launch-contract.execution-owner.invalid.json +19 -0
- package/scripts/fixtures/contracts/limited-reason.unbounded.invalid.json +11 -0
- package/scripts/fixtures/contracts/limited-reason.valid.json +11 -0
- package/scripts/fixtures/contracts/operation-authority-requirement.no-issuer.invalid.json +22 -0
- package/scripts/fixtures/contracts/operation-authority-requirement.valid.json +22 -0
- package/scripts/fixtures/contracts/operation-grant.missing-issuer-evidence.invalid.json +34 -0
- package/scripts/fixtures/contracts/operation-grant.valid.json +34 -0
- package/scripts/fixtures/contracts/phase-slice-correlation.aggregate-closure.invalid.json +12 -0
- package/scripts/fixtures/contracts/phase-slice-correlation.valid.json +12 -0
- package/scripts/fixtures/contracts/profile-change-candidate.unconfirmed.invalid.json +13 -0
- package/scripts/fixtures/contracts/profile-explanation.false-complete.invalid.json +36 -0
- package/scripts/fixtures/contracts/profile-explanation.valid.json +29 -0
- package/scripts/fixtures/contracts/provider-adapter-observation.unauthenticated-supported.invalid.json +43 -0
- package/scripts/fixtures/contracts/provider-adapter-observation.valid.json +62 -0
- package/scripts/fixtures/contracts/return-receipt-v2.changed-subject.valid.json +36 -0
- package/scripts/fixtures/contracts/return-receipt-v2.unverified-closeout.invalid.json +36 -0
- package/scripts/fixtures/contracts/return-receipt-v2.valid.json +36 -0
- package/scripts/fixtures/contracts/review-authority-requirement.transitive.invalid.json +33 -0
- package/scripts/fixtures/contracts/review-authority-requirement.valid.json +21 -0
- package/scripts/fixtures/contracts/rollback-envelope-lineage.reused-grant.invalid.json +24 -0
- package/scripts/fixtures/contracts/rollback-envelope-lineage.valid.json +24 -0
- package/scripts/fixtures/contracts/rule-claim.inferred-enforceable.invalid.json +16 -0
- package/scripts/fixtures/contracts/rule-claim.valid.json +16 -0
- package/scripts/fixtures/contracts/source-dependency-edge.unavailable-without-reason.invalid.json +18 -0
- package/scripts/fixtures/contracts/source-dependency-edge.valid.json +18 -0
- package/scripts/fixtures/contracts/task-control-revision.closed-without-receipt.invalid.json +37 -0
- package/scripts/fixtures/contracts/task-control-revision.valid.json +37 -0
- package/scripts/fixtures/contracts/task-envelope-v2.missing-workspace-identity.invalid.json +39 -0
- package/scripts/fixtures/contracts/task-envelope-v2.valid.json +54 -0
- package/scripts/fixtures/contracts/task-envelope-v2.workspace-mismatch.invalid.json +40 -0
- package/scripts/fixtures/contracts/validation-result.passed-without-evidence.invalid.json +29 -0
- package/scripts/fixtures/contracts/validation-result.valid.json +19 -0
- package/scripts/fixtures/contracts/validation-topology.dangling-requirement.invalid.json +32 -0
- package/scripts/fixtures/contracts/validation-topology.protected-environment.valid.json +32 -0
- package/scripts/fixtures/contracts/validation-topology.valid.json +32 -0
- package/scripts/fixtures/contracts/workspace-identity.missing-root.invalid.json +6 -0
- package/scripts/fixtures/contracts/workspace-identity.valid.json +14 -0
- package/scripts/fixtures/contracts/workspace-relationship.inferred-authority.invalid.json +13 -0
- package/scripts/fixtures/contracts/workspace-relationship.valid.json +13 -0
- package/scripts/fixtures/control-system/architecture-convergence-cases.json +25 -0
- package/scripts/fixtures/control-system/task-2.9-invalid-boundary-matrix.json +203 -0
- package/scripts/fixtures/control-system/task-4-diagnosis-health-separation.json +19 -0
- package/scripts/fixtures/workspaces/no-node-project/AGENTS.md +3 -0
- package/scripts/fixtures/workspaces/no-node-project/README.md +3 -0
- package/scripts/fixtures/workspaces/no-node-project/docs/adr/0001-durable-records.md +3 -0
- package/scripts/forgerail.mjs +467 -14
- package/scripts/lib/contracts.mjs +1132 -2
- package/scripts/shadow-comparison.mjs +1 -1
- package/scripts/validate-release.mjs +15 -15
- package/scripts/validate-universal-directory.mjs +151 -0
- package/skills/architecture-convergence-audit/SKILL.md +31 -0
- package/skills/architecture-convergence-audit/agents/openai.yaml +7 -0
- package/skills/architecture-convergence-audit/assets/github-issue-template.md +26 -0
- package/skills/architecture-convergence-audit/references/audit-contract.md +62 -0
- package/skills/workspace-health-review/references/health-review.md +15 -0
|
@@ -1,7 +1,45 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
|
|
4
|
-
export const
|
|
4
|
+
export const contractSchemaNames = {
|
|
5
|
+
"adoption-plan": "adoption-plan",
|
|
6
|
+
"authority-evidence": "authority-evidence-v1",
|
|
7
|
+
"binding-receipt": "host-binding-receipt",
|
|
8
|
+
"cross-workspace-pack-composition": "cross-workspace-pack-composition-v1",
|
|
9
|
+
"task-control-revision": "task-control-revision-v1",
|
|
10
|
+
"task-envelope-v2": "task-envelope-v2",
|
|
11
|
+
"entry-mode": "entry-mode-v1",
|
|
12
|
+
envelope: "task-envelope",
|
|
13
|
+
"effective-profile-v2": "effective-profile-v2",
|
|
14
|
+
"evidence-identity": "evidence-identity-v1",
|
|
15
|
+
"execution-context-identity": "execution-context-identity-v1",
|
|
16
|
+
"gate-result": "gate-result-v1",
|
|
17
|
+
"governance-source": "governance-source-v1",
|
|
18
|
+
"host-adapter": "host-adapter",
|
|
19
|
+
"host-adapter-observation": "host-adapter-observation-v1",
|
|
20
|
+
launch: "launch-contract",
|
|
21
|
+
"limited-reason": "limited-reason-v1",
|
|
22
|
+
"operation-authority-requirement": "operation-authority-requirement-v1",
|
|
23
|
+
"operation-grant": "operation-grant-v1",
|
|
24
|
+
pack: "capability-pack",
|
|
25
|
+
"phase-slice-correlation": "phase-slice-correlation-v1",
|
|
26
|
+
profile: "effective-profile",
|
|
27
|
+
"profile-candidate": "profile-change-candidate",
|
|
28
|
+
"profile-explanation": "profile-explanation-v1",
|
|
29
|
+
"provider-adapter-observation": "provider-adapter-observation-v1",
|
|
30
|
+
receipt: "return-receipt",
|
|
31
|
+
"review-authority-requirement": "review-authority-requirement-v1",
|
|
32
|
+
"return-receipt-v2": "return-receipt-v2",
|
|
33
|
+
"rollback-envelope-lineage": "rollback-envelope-lineage-v1",
|
|
34
|
+
"rule-claim": "rule-claim-v1",
|
|
35
|
+
"source-dependency-edge": "source-dependency-edge-v1",
|
|
36
|
+
"validation-result": "validation-result-v1",
|
|
37
|
+
"validation-topology": "validation-topology-v1",
|
|
38
|
+
"workspace-identity": "workspace-identity-v1",
|
|
39
|
+
"workspace-relationship": "workspace-relationship-v1"
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const contractTypes = Object.keys(contractSchemaNames);
|
|
5
43
|
|
|
6
44
|
const packStates = ["available", "recommended", "enabled", "required", "blocked", "disabled"];
|
|
7
45
|
const idPattern = /^[a-z][a-z0-9-]+$/;
|
|
@@ -9,6 +47,16 @@ const ruleIdPattern = /^[a-z][a-z0-9.-]+$/;
|
|
|
9
47
|
const taskIdPattern = /^[a-zA-Z0-9._:-]+$/;
|
|
10
48
|
const commitPattern = /^[0-9a-f]{40}$/;
|
|
11
49
|
const digestPattern = /^[0-9a-f]{64}$/;
|
|
50
|
+
const authorityClasses = ["agent_review", "automated_validation", "peer_review", "ownership_approval", "security_approval", "release_approval", "environment_approval"];
|
|
51
|
+
const trustClasses = ["self-reported", "local-observed", "provider-attested", "trusted-runner", "protected-environment"];
|
|
52
|
+
const adapterSupportStates = ["supported", "profile-only", "degraded", "blocked", "unavailable", "unknown"];
|
|
53
|
+
const limitedReasonCodes = [
|
|
54
|
+
"missing-required-evidence", "unavailable-provider", "unauthenticated-provider", "unverified-adapter",
|
|
55
|
+
"stale-evidence", "revoked-evidence", "dismissed-evidence", "expired-evidence", "superseded-evidence",
|
|
56
|
+
"wrong-subject", "wrong-trust", "unclassified-surface", "missing-result", "unexpected-result",
|
|
57
|
+
"dangling-dependency", "blocked-prerequisite", "boundary-crossing-dependency", "redacted-sensitive-detail",
|
|
58
|
+
"unsupported-capability", "other"
|
|
59
|
+
];
|
|
12
60
|
|
|
13
61
|
function object(value) {
|
|
14
62
|
return value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -35,6 +83,11 @@ function nullableString(value, label, errors, pattern) {
|
|
|
35
83
|
string(value, label, errors, pattern);
|
|
36
84
|
}
|
|
37
85
|
|
|
86
|
+
function dateTime(value, label, errors) {
|
|
87
|
+
string(value, label, errors);
|
|
88
|
+
if (typeof value === "string" && (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) || Number.isNaN(Date.parse(value)))) errors.push(`${label} must be an ISO 8601 date-time with timezone`);
|
|
89
|
+
}
|
|
90
|
+
|
|
38
91
|
function strings(value, label, errors, { min = 0, pattern, unique = false } = {}) {
|
|
39
92
|
if (!Array.isArray(value)) {
|
|
40
93
|
errors.push(`${label} must be an array`);
|
|
@@ -53,6 +106,54 @@ function sha256(value) {
|
|
|
53
106
|
return createHash("sha256").update(value).digest("hex");
|
|
54
107
|
}
|
|
55
108
|
|
|
109
|
+
function contractVersion(value, expected, label, errors) {
|
|
110
|
+
if (value !== expected) errors.push(`${label}.schemaVersion must equal ${expected}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function validateTarget(value, label, errors) {
|
|
114
|
+
if (!exactKeys(value, ["kind", "locator", "identity", "ref", "environment"], [], label, errors)) return;
|
|
115
|
+
if (!["file", "directory", "repository", "ref", "artifact", "environment", "profile-source", "other"].includes(value.kind)) errors.push(`${label}.kind is invalid`);
|
|
116
|
+
string(value.locator, `${label}.locator`, errors);
|
|
117
|
+
nullableString(value.identity, `${label}.identity`, errors);
|
|
118
|
+
nullableString(value.ref, `${label}.ref`, errors);
|
|
119
|
+
nullableString(value.environment, `${label}.environment`, errors);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function validateSubject(value, label, errors) {
|
|
123
|
+
if (!exactKeys(value, ["subjectId", "kind", "digest", "locator"], [], label, errors)) return;
|
|
124
|
+
string(value.subjectId, `${label}.subjectId`, errors, taskIdPattern);
|
|
125
|
+
if (!["commit", "tree", "diff", "artifact", "candidate", "workspace-state", "other"].includes(value.kind)) errors.push(`${label}.kind is invalid`);
|
|
126
|
+
string(value.digest, `${label}.digest`, errors, digestPattern);
|
|
127
|
+
nullableString(value.locator, `${label}.locator`, errors);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function validateEnvelopeReference(value, label, errors) {
|
|
131
|
+
if (!exactKeys(value, ["envelopeId", "revisionId", "subjectId"], [], label, errors)) return;
|
|
132
|
+
string(value.envelopeId, `${label}.envelopeId`, errors, taskIdPattern);
|
|
133
|
+
string(value.revisionId, `${label}.revisionId`, errors, taskIdPattern);
|
|
134
|
+
string(value.subjectId, `${label}.subjectId`, errors, taskIdPattern);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validateLimitedReason(value, errors, label = "limitedReason") {
|
|
138
|
+
const keys = ["schemaVersion", "reasonId", "code", "impact", "summary", "evidenceIdentityIds", "locators", "observedAt", "sanitized"];
|
|
139
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
140
|
+
contractVersion(value.schemaVersion, "1.0", label, errors);
|
|
141
|
+
string(value.reasonId, `${label}.reasonId`, errors, taskIdPattern);
|
|
142
|
+
if (!limitedReasonCodes.includes(value.code)) errors.push(`${label}.code is invalid`);
|
|
143
|
+
if (!["degraded", "blocked", "unavailable", "unresolved", "unverified"].includes(value.impact)) errors.push(`${label}.impact is invalid`);
|
|
144
|
+
string(value.summary, `${label}.summary`, errors);
|
|
145
|
+
if (typeof value.summary === "string" && value.summary.length > 500) errors.push(`${label}.summary must not exceed 500 characters`);
|
|
146
|
+
strings(value.evidenceIdentityIds, `${label}.evidenceIdentityIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
147
|
+
strings(value.locators, `${label}.locators`, errors, { unique: true });
|
|
148
|
+
dateTime(value.observedAt, `${label}.observedAt`, errors);
|
|
149
|
+
if (value.sanitized !== true) errors.push(`${label}.sanitized must equal true`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function validateNullableLimitedReason(value, errors, label) {
|
|
153
|
+
if (value === null) return;
|
|
154
|
+
validateLimitedReason(value, errors, label);
|
|
155
|
+
}
|
|
156
|
+
|
|
56
157
|
function validatePack(value, errors) {
|
|
57
158
|
const keys = ["schemaVersion", "id", "purpose", "entry", "risk", "states", "triggers", "inputs", "dependencies", "conflicts", "approvals", "validation", "receiptExtensions"];
|
|
58
159
|
if (!exactKeys(value, keys, [], "pack", errors)) return;
|
|
@@ -304,10 +405,1039 @@ function validateBindingReceipt(value, errors) {
|
|
|
304
405
|
}
|
|
305
406
|
}
|
|
306
407
|
|
|
408
|
+
function validateWorkspaceIdentity(value, errors) {
|
|
409
|
+
const keys = ["schemaVersion", "workspaceIdentityId", "canonicalRootLocator", "boundaryClaims", "observedAt"];
|
|
410
|
+
if (!exactKeys(value, keys, [], "workspaceIdentity", errors)) return;
|
|
411
|
+
schemaVersion(value.schemaVersion, "workspaceIdentity", errors);
|
|
412
|
+
string(value.workspaceIdentityId, "workspaceIdentity.workspaceIdentityId", errors, taskIdPattern);
|
|
413
|
+
string(value.canonicalRootLocator, "workspaceIdentity.canonicalRootLocator", errors);
|
|
414
|
+
if (!Array.isArray(value.boundaryClaims)) errors.push("workspaceIdentity.boundaryClaims must be an array");
|
|
415
|
+
else value.boundaryClaims.forEach((claim, index) => {
|
|
416
|
+
const label = `workspaceIdentity.boundaryClaims[${index}]`;
|
|
417
|
+
if (!exactKeys(claim, ["providerId", "kind", "identity", "observedAt"], [], label, errors)) return;
|
|
418
|
+
string(claim.providerId, `${label}.providerId`, errors, idPattern);
|
|
419
|
+
if (!["repository", "vcs-root", "worktree", "explicit-workspace", "provider-project"].includes(claim.kind)) errors.push(`${label}.kind is invalid`);
|
|
420
|
+
string(claim.identity, `${label}.identity`, errors);
|
|
421
|
+
dateTime(claim.observedAt, `${label}.observedAt`, errors);
|
|
422
|
+
});
|
|
423
|
+
const boundaryKeys = value.boundaryClaims?.map((claim) => `${claim.providerId}:${claim.kind}:${claim.identity}`) ?? [];
|
|
424
|
+
if (new Set(boundaryKeys).size !== boundaryKeys.length) errors.push("workspaceIdentity.boundaryClaims contains duplicate identities");
|
|
425
|
+
dateTime(value.observedAt, "workspaceIdentity.observedAt", errors);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function validateWorkspaceRelationship(value, errors) {
|
|
429
|
+
const keys = ["schemaVersion", "relationshipId", "sourceWorkspaceIdentityId", "targetWorkspaceIdentityId", "relationshipType", "governanceApplicability", "applicabilityScope", "declaredBySourceId", "provenanceStatus", "authorityTransfer", "observedAt"];
|
|
430
|
+
if (!exactKeys(value, keys, [], "workspaceRelationship", errors)) return;
|
|
431
|
+
schemaVersion(value.schemaVersion, "workspaceRelationship", errors);
|
|
432
|
+
string(value.relationshipId, "workspaceRelationship.relationshipId", errors, taskIdPattern);
|
|
433
|
+
string(value.sourceWorkspaceIdentityId, "workspaceRelationship.sourceWorkspaceIdentityId", errors, taskIdPattern);
|
|
434
|
+
string(value.targetWorkspaceIdentityId, "workspaceRelationship.targetWorkspaceIdentityId", errors, taskIdPattern);
|
|
435
|
+
if (value.sourceWorkspaceIdentityId === value.targetWorkspaceIdentityId) errors.push("workspaceRelationship must relate distinct Workspace Identities");
|
|
436
|
+
if (!["governance-scope", "coordination", "dependency", "ownership-reference"].includes(value.relationshipType)) errors.push("workspaceRelationship.relationshipType is invalid");
|
|
437
|
+
if (!["none", "scoped"].includes(value.governanceApplicability)) errors.push("workspaceRelationship.governanceApplicability is invalid");
|
|
438
|
+
strings(value.applicabilityScope, "workspaceRelationship.applicabilityScope", errors, { unique: true });
|
|
439
|
+
string(value.declaredBySourceId, "workspaceRelationship.declaredBySourceId", errors, taskIdPattern);
|
|
440
|
+
if (!["provider-declared", "confirmed", "inferred", "requires-confirmation"].includes(value.provenanceStatus)) errors.push("workspaceRelationship.provenanceStatus is invalid");
|
|
441
|
+
if (value.authorityTransfer !== false) errors.push("workspaceRelationship.authorityTransfer must equal false");
|
|
442
|
+
if (["inferred", "requires-confirmation"].includes(value.provenanceStatus) && value.governanceApplicability !== "none") errors.push("inferred workspaceRelationship cannot apply governance");
|
|
443
|
+
if (value.governanceApplicability === "scoped" && (value.applicabilityScope?.length ?? 0) === 0) errors.push("scoped workspaceRelationship requires applicabilityScope");
|
|
444
|
+
dateTime(value.observedAt, "workspaceRelationship.observedAt", errors);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function validateGovernanceSource(value, errors) {
|
|
448
|
+
const keys = ["schemaVersion", "sourceId", "workspaceIdentityId", "sourceKind", "locator", "ownerId", "precedenceClass", "applicabilityScope", "workspaceRelationshipIds", "observationStatus", "ruleClaimIds", "dependencyEdgeIds", "observedAt", "limitedReason"];
|
|
449
|
+
if (!exactKeys(value, keys, [], "governanceSource", errors)) return;
|
|
450
|
+
schemaVersion(value.schemaVersion, "governanceSource", errors);
|
|
451
|
+
string(value.sourceId, "governanceSource.sourceId", errors, taskIdPattern);
|
|
452
|
+
string(value.workspaceIdentityId, "governanceSource.workspaceIdentityId", errors, taskIdPattern);
|
|
453
|
+
if (!["instructions", "ownership", "platform-policy", "ci", "script", "specification", "decision-record", "structured-profile", "other"].includes(value.sourceKind)) errors.push("governanceSource.sourceKind is invalid");
|
|
454
|
+
string(value.locator, "governanceSource.locator", errors);
|
|
455
|
+
nullableString(value.ownerId, "governanceSource.ownerId", errors);
|
|
456
|
+
string(value.precedenceClass, "governanceSource.precedenceClass", errors);
|
|
457
|
+
strings(value.applicabilityScope, "governanceSource.applicabilityScope", errors, { min: 1, unique: true });
|
|
458
|
+
strings(value.workspaceRelationshipIds, "governanceSource.workspaceRelationshipIds", errors, { pattern: taskIdPattern, unique: true });
|
|
459
|
+
if (!["observed", "unavailable", "ambiguous", "unverified"].includes(value.observationStatus)) errors.push("governanceSource.observationStatus is invalid");
|
|
460
|
+
strings(value.ruleClaimIds, "governanceSource.ruleClaimIds", errors, { pattern: taskIdPattern, unique: true });
|
|
461
|
+
strings(value.dependencyEdgeIds, "governanceSource.dependencyEdgeIds", errors, { pattern: taskIdPattern, unique: true });
|
|
462
|
+
dateTime(value.observedAt, "governanceSource.observedAt", errors);
|
|
463
|
+
nullableString(value.limitedReason, "governanceSource.limitedReason", errors);
|
|
464
|
+
if (["unavailable", "ambiguous", "unverified"].includes(value.observationStatus) && (typeof value.limitedReason !== "string" || value.limitedReason.length === 0)) errors.push("governanceSource.limitedReason is required when source is not observed");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function validateSourceDependencyEdge(value, errors) {
|
|
468
|
+
const keys = ["schemaVersion", "edgeId", "workspaceIdentityId", "declaringSourceId", "target", "requiredness", "applicabilityScope", "provenanceStatus", "observationStatus", "affectedClaimIds", "observedAt", "limitedReason"];
|
|
469
|
+
if (!exactKeys(value, keys, [], "sourceDependencyEdge", errors)) return;
|
|
470
|
+
schemaVersion(value.schemaVersion, "sourceDependencyEdge", errors);
|
|
471
|
+
string(value.edgeId, "sourceDependencyEdge.edgeId", errors, taskIdPattern);
|
|
472
|
+
string(value.workspaceIdentityId, "sourceDependencyEdge.workspaceIdentityId", errors, taskIdPattern);
|
|
473
|
+
string(value.declaringSourceId, "sourceDependencyEdge.declaringSourceId", errors, taskIdPattern);
|
|
474
|
+
if (exactKeys(value.target, ["kind", "locator", "identity"], [], "sourceDependencyEdge.target", errors)) {
|
|
475
|
+
if (!["skill", "pack", "command", "script", "record-entry", "source", "schema", "other"].includes(value.target.kind)) errors.push("sourceDependencyEdge.target.kind is invalid");
|
|
476
|
+
string(value.target.locator, "sourceDependencyEdge.target.locator", errors);
|
|
477
|
+
nullableString(value.target.identity, "sourceDependencyEdge.target.identity", errors);
|
|
478
|
+
}
|
|
479
|
+
if (!["required", "optional"].includes(value.requiredness)) errors.push("sourceDependencyEdge.requiredness is invalid");
|
|
480
|
+
strings(value.applicabilityScope, "sourceDependencyEdge.applicabilityScope", errors, { min: 1, unique: true });
|
|
481
|
+
if (!["structured", "provider-declared", "confirmed", "inferred", "requires-confirmation"].includes(value.provenanceStatus)) errors.push("sourceDependencyEdge.provenanceStatus is invalid");
|
|
482
|
+
if (!["available", "unavailable", "ambiguous", "unverified"].includes(value.observationStatus)) errors.push("sourceDependencyEdge.observationStatus is invalid");
|
|
483
|
+
strings(value.affectedClaimIds, "sourceDependencyEdge.affectedClaimIds", errors, { pattern: taskIdPattern, unique: true });
|
|
484
|
+
dateTime(value.observedAt, "sourceDependencyEdge.observedAt", errors);
|
|
485
|
+
nullableString(value.limitedReason, "sourceDependencyEdge.limitedReason", errors);
|
|
486
|
+
if (value.observationStatus !== "available" && (typeof value.limitedReason !== "string" || value.limitedReason.length === 0)) errors.push("sourceDependencyEdge.limitedReason is required when dependency is not available");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function validateRuleClaim(value, errors, label = "ruleClaim") {
|
|
490
|
+
const keys = ["schemaVersion", "claimId", "workspaceIdentityId", "sourceId", "ruleKey", "normalizedValue", "precedenceClass", "applicabilityScope", "observationPoint", "status", "enforcement", "dependencyEdgeIds", "observedAt", "limitedReason"];
|
|
491
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
492
|
+
schemaVersion(value.schemaVersion, label, errors);
|
|
493
|
+
string(value.claimId, `${label}.claimId`, errors, taskIdPattern);
|
|
494
|
+
string(value.workspaceIdentityId, `${label}.workspaceIdentityId`, errors, taskIdPattern);
|
|
495
|
+
string(value.sourceId, `${label}.sourceId`, errors, taskIdPattern);
|
|
496
|
+
string(value.ruleKey, `${label}.ruleKey`, errors, ruleIdPattern);
|
|
497
|
+
string(value.precedenceClass, `${label}.precedenceClass`, errors);
|
|
498
|
+
strings(value.applicabilityScope, `${label}.applicabilityScope`, errors, { min: 1, unique: true });
|
|
499
|
+
string(value.observationPoint, `${label}.observationPoint`, errors);
|
|
500
|
+
if (!["observed", "inferred", "confirmed", "default", "requires-confirmation"].includes(value.status)) errors.push(`${label}.status is invalid`);
|
|
501
|
+
if (!["enforceable", "advisory", "unresolved"].includes(value.enforcement)) errors.push(`${label}.enforcement is invalid`);
|
|
502
|
+
if (["inferred", "requires-confirmation"].includes(value.status) && value.enforcement === "enforceable") errors.push(`${label} inferred claim cannot be enforceable`);
|
|
503
|
+
strings(value.dependencyEdgeIds, `${label}.dependencyEdgeIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
504
|
+
dateTime(value.observedAt, `${label}.observedAt`, errors);
|
|
505
|
+
nullableString(value.limitedReason, `${label}.limitedReason`, errors);
|
|
506
|
+
if (value.enforcement === "unresolved" && (typeof value.limitedReason !== "string" || value.limitedReason.length === 0)) errors.push(`${label}.limitedReason is required when enforcement is unresolved`);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function validateEffectiveProfileV2(value, errors) {
|
|
510
|
+
const keys = ["schemaVersion", "profileId", "revisionId", "workspaceIdentityId", "computed", "computedAt", "completeness", "sourceIds", "workspaceRelationshipIds", "ruleClaims", "dependencyEdges", "applicablePackIds", "conflicts", "explanationId", "limitedReasons"];
|
|
511
|
+
if (!exactKeys(value, keys, [], "effectiveProfileV2", errors)) return;
|
|
512
|
+
if (value.schemaVersion !== "2.0") errors.push("effectiveProfileV2.schemaVersion must equal 2.0");
|
|
513
|
+
string(value.profileId, "effectiveProfileV2.profileId", errors, taskIdPattern);
|
|
514
|
+
string(value.revisionId, "effectiveProfileV2.revisionId", errors, taskIdPattern);
|
|
515
|
+
string(value.workspaceIdentityId, "effectiveProfileV2.workspaceIdentityId", errors, taskIdPattern);
|
|
516
|
+
if (value.computed !== true) errors.push("effectiveProfileV2.computed must equal true");
|
|
517
|
+
dateTime(value.computedAt, "effectiveProfileV2.computedAt", errors);
|
|
518
|
+
if (!["complete", "degraded", "unresolved"].includes(value.completeness)) errors.push("effectiveProfileV2.completeness is invalid");
|
|
519
|
+
strings(value.sourceIds, "effectiveProfileV2.sourceIds", errors, { pattern: taskIdPattern, unique: true });
|
|
520
|
+
strings(value.workspaceRelationshipIds, "effectiveProfileV2.workspaceRelationshipIds", errors, { pattern: taskIdPattern, unique: true });
|
|
521
|
+
if (!Array.isArray(value.ruleClaims)) errors.push("effectiveProfileV2.ruleClaims must be an array");
|
|
522
|
+
else value.ruleClaims.forEach((claim, index) => validateRuleClaim(claim, errors, `effectiveProfileV2.ruleClaims[${index}]`));
|
|
523
|
+
if (!Array.isArray(value.dependencyEdges)) errors.push("effectiveProfileV2.dependencyEdges must be an array");
|
|
524
|
+
else value.dependencyEdges.forEach((edge, index) => {
|
|
525
|
+
const nestedErrors = [];
|
|
526
|
+
validateSourceDependencyEdge(edge, nestedErrors);
|
|
527
|
+
errors.push(...nestedErrors.map((error) => error.replace(/^sourceDependencyEdge/, `effectiveProfileV2.dependencyEdges[${index}]`)));
|
|
528
|
+
});
|
|
529
|
+
strings(value.applicablePackIds, "effectiveProfileV2.applicablePackIds", errors, { pattern: idPattern, unique: true });
|
|
530
|
+
if (!Array.isArray(value.conflicts)) errors.push("effectiveProfileV2.conflicts must be an array");
|
|
531
|
+
else value.conflicts.forEach((conflict, index) => {
|
|
532
|
+
const label = `effectiveProfileV2.conflicts[${index}]`;
|
|
533
|
+
if (!exactKeys(conflict, ["conflictId", "claimIds", "status", "limitedReason"], [], label, errors)) return;
|
|
534
|
+
string(conflict.conflictId, `${label}.conflictId`, errors, taskIdPattern);
|
|
535
|
+
strings(conflict.claimIds, `${label}.claimIds`, errors, { min: 2, pattern: taskIdPattern, unique: true });
|
|
536
|
+
if (conflict.status !== "unresolved") errors.push(`${label}.status must equal unresolved`);
|
|
537
|
+
string(conflict.limitedReason, `${label}.limitedReason`, errors);
|
|
538
|
+
});
|
|
539
|
+
string(value.explanationId, "effectiveProfileV2.explanationId", errors, taskIdPattern);
|
|
540
|
+
strings(value.limitedReasons, "effectiveProfileV2.limitedReasons", errors, { unique: true });
|
|
541
|
+
|
|
542
|
+
const sourceIds = new Set(value.sourceIds ?? []);
|
|
543
|
+
const claimIds = value.ruleClaims?.map((claim) => claim.claimId) ?? [];
|
|
544
|
+
const edgeIds = value.dependencyEdges?.map((edge) => edge.edgeId) ?? [];
|
|
545
|
+
if (new Set(claimIds).size !== claimIds.length) errors.push("effectiveProfileV2.ruleClaims contains duplicate claim ids");
|
|
546
|
+
if (new Set(edgeIds).size !== edgeIds.length) errors.push("effectiveProfileV2.dependencyEdges contains duplicate edge ids");
|
|
547
|
+
const conflictIds = value.conflicts?.map((conflict) => conflict.conflictId) ?? [];
|
|
548
|
+
if (new Set(conflictIds).size !== conflictIds.length) errors.push("effectiveProfileV2.conflicts contains duplicate conflict ids");
|
|
549
|
+
const claimSet = new Set(claimIds);
|
|
550
|
+
const edgeSet = new Set(edgeIds);
|
|
551
|
+
for (const claim of value.ruleClaims ?? []) {
|
|
552
|
+
if (claim.workspaceIdentityId !== value.workspaceIdentityId) errors.push(`effectiveProfileV2 claim Workspace Identity mismatch: ${claim.claimId}`);
|
|
553
|
+
if (!sourceIds.has(claim.sourceId)) errors.push(`effectiveProfileV2 claim source is not inventoried: ${claim.claimId}`);
|
|
554
|
+
for (const edgeId of claim.dependencyEdgeIds ?? []) if (!edgeSet.has(edgeId)) errors.push(`effectiveProfileV2 claim references unknown dependency edge: ${edgeId}`);
|
|
555
|
+
}
|
|
556
|
+
for (const edge of value.dependencyEdges ?? []) {
|
|
557
|
+
if (edge.workspaceIdentityId !== value.workspaceIdentityId) errors.push(`effectiveProfileV2 dependency Workspace Identity mismatch: ${edge.edgeId}`);
|
|
558
|
+
if (!sourceIds.has(edge.declaringSourceId)) errors.push(`effectiveProfileV2 dependency source is not inventoried: ${edge.edgeId}`);
|
|
559
|
+
for (const claimId of edge.affectedClaimIds ?? []) if (!claimSet.has(claimId)) errors.push(`effectiveProfileV2 dependency references unknown claim: ${claimId}`);
|
|
560
|
+
}
|
|
561
|
+
for (const conflict of value.conflicts ?? []) for (const claimId of conflict.claimIds ?? []) if (!claimSet.has(claimId)) errors.push(`effectiveProfileV2 conflict references unknown claim: ${claimId}`);
|
|
562
|
+
const requiredUnavailable = (value.dependencyEdges ?? []).some((edge) => edge.requiredness === "required" && edge.observationStatus !== "available");
|
|
563
|
+
const anyUnavailable = (value.dependencyEdges ?? []).some((edge) => edge.observationStatus !== "available");
|
|
564
|
+
const unresolvedClaim = (value.ruleClaims ?? []).some((claim) => claim.enforcement === "unresolved");
|
|
565
|
+
if (value.completeness === "complete" && ((value.conflicts?.length ?? 0) > 0 || (value.limitedReasons?.length ?? 0) > 0 || anyUnavailable || unresolvedClaim)) errors.push("complete effectiveProfileV2 cannot contain unresolved, unavailable, conflict, or limited state");
|
|
566
|
+
if (["degraded", "unresolved"].includes(value.completeness) && (value.limitedReasons?.length ?? 0) === 0) errors.push("non-complete effectiveProfileV2 requires limitedReasons");
|
|
567
|
+
if ((requiredUnavailable || unresolvedClaim || (value.conflicts?.length ?? 0) > 0) && value.completeness !== "unresolved") errors.push("required dependency, unresolved claim, or conflict requires unresolved effectiveProfileV2");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function validateProfileExplanation(value, errors) {
|
|
571
|
+
const keys = ["schemaVersion", "explanationId", "profileId", "profileRevisionId", "workspaceIdentityId", "generatedAt", "completeness", "sourceSummaries", "claimDecisions", "conflicts", "confirmationNeeds", "limitedReasons"];
|
|
572
|
+
if (!exactKeys(value, keys, [], "profileExplanation", errors)) return;
|
|
573
|
+
schemaVersion(value.schemaVersion, "profileExplanation", errors);
|
|
574
|
+
string(value.explanationId, "profileExplanation.explanationId", errors, taskIdPattern);
|
|
575
|
+
string(value.profileId, "profileExplanation.profileId", errors, taskIdPattern);
|
|
576
|
+
string(value.profileRevisionId, "profileExplanation.profileRevisionId", errors, taskIdPattern);
|
|
577
|
+
string(value.workspaceIdentityId, "profileExplanation.workspaceIdentityId", errors, taskIdPattern);
|
|
578
|
+
dateTime(value.generatedAt, "profileExplanation.generatedAt", errors);
|
|
579
|
+
if (!["complete", "degraded", "unresolved"].includes(value.completeness)) errors.push("profileExplanation.completeness is invalid");
|
|
580
|
+
if (!Array.isArray(value.sourceSummaries)) errors.push("profileExplanation.sourceSummaries must be an array");
|
|
581
|
+
else value.sourceSummaries.forEach((source, index) => {
|
|
582
|
+
const label = `profileExplanation.sourceSummaries[${index}]`;
|
|
583
|
+
if (!exactKeys(source, ["sourceId", "status", "claimIds", "limitedReason"], [], label, errors)) return;
|
|
584
|
+
string(source.sourceId, `${label}.sourceId`, errors, taskIdPattern);
|
|
585
|
+
if (!["observed", "unavailable", "ambiguous", "unverified"].includes(source.status)) errors.push(`${label}.status is invalid`);
|
|
586
|
+
strings(source.claimIds, `${label}.claimIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
587
|
+
nullableString(source.limitedReason, `${label}.limitedReason`, errors);
|
|
588
|
+
if (source.status !== "observed" && (typeof source.limitedReason !== "string" || source.limitedReason.length === 0)) errors.push(`${label}.limitedReason is required when source is not observed`);
|
|
589
|
+
});
|
|
590
|
+
if (!Array.isArray(value.claimDecisions)) errors.push("profileExplanation.claimDecisions must be an array");
|
|
591
|
+
else value.claimDecisions.forEach((decision, index) => {
|
|
592
|
+
const label = `profileExplanation.claimDecisions[${index}]`;
|
|
593
|
+
if (!exactKeys(decision, ["claimId", "sourceId", "disposition", "reasonCode", "dependencyEdgeIds"], [], label, errors)) return;
|
|
594
|
+
string(decision.claimId, `${label}.claimId`, errors, taskIdPattern);
|
|
595
|
+
string(decision.sourceId, `${label}.sourceId`, errors, taskIdPattern);
|
|
596
|
+
if (!["active", "shadowed", "unresolved", "excluded"].includes(decision.disposition)) errors.push(`${label}.disposition is invalid`);
|
|
597
|
+
string(decision.reasonCode, `${label}.reasonCode`, errors);
|
|
598
|
+
strings(decision.dependencyEdgeIds, `${label}.dependencyEdgeIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
599
|
+
});
|
|
600
|
+
if (!Array.isArray(value.conflicts)) errors.push("profileExplanation.conflicts must be an array");
|
|
601
|
+
else value.conflicts.forEach((conflict, index) => {
|
|
602
|
+
const label = `profileExplanation.conflicts[${index}]`;
|
|
603
|
+
if (!exactKeys(conflict, ["conflictId", "claimIds", "limitedReason", "confirmationRequired"], [], label, errors)) return;
|
|
604
|
+
string(conflict.conflictId, `${label}.conflictId`, errors, taskIdPattern);
|
|
605
|
+
strings(conflict.claimIds, `${label}.claimIds`, errors, { min: 2, pattern: taskIdPattern, unique: true });
|
|
606
|
+
string(conflict.limitedReason, `${label}.limitedReason`, errors);
|
|
607
|
+
if (typeof conflict.confirmationRequired !== "boolean") errors.push(`${label}.confirmationRequired must be a boolean`);
|
|
608
|
+
});
|
|
609
|
+
if (!Array.isArray(value.confirmationNeeds)) errors.push("profileExplanation.confirmationNeeds must be an array");
|
|
610
|
+
else value.confirmationNeeds.forEach((need, index) => {
|
|
611
|
+
const label = `profileExplanation.confirmationNeeds[${index}]`;
|
|
612
|
+
if (!exactKeys(need, ["needId", "claimIds", "reasonCode", "prompt"], [], label, errors)) return;
|
|
613
|
+
string(need.needId, `${label}.needId`, errors, taskIdPattern);
|
|
614
|
+
strings(need.claimIds, `${label}.claimIds`, errors, { min: 1, pattern: taskIdPattern, unique: true });
|
|
615
|
+
string(need.reasonCode, `${label}.reasonCode`, errors);
|
|
616
|
+
string(need.prompt, `${label}.prompt`, errors);
|
|
617
|
+
});
|
|
618
|
+
strings(value.limitedReasons, "profileExplanation.limitedReasons", errors, { unique: true });
|
|
619
|
+
const sourceIds = value.sourceSummaries?.map((source) => source.sourceId) ?? [];
|
|
620
|
+
const claimIds = value.claimDecisions?.map((decision) => decision.claimId) ?? [];
|
|
621
|
+
const conflictIds = value.conflicts?.map((conflict) => conflict.conflictId) ?? [];
|
|
622
|
+
const needIds = value.confirmationNeeds?.map((need) => need.needId) ?? [];
|
|
623
|
+
if (new Set(sourceIds).size !== sourceIds.length) errors.push("profileExplanation.sourceSummaries contains duplicate source ids");
|
|
624
|
+
if (new Set(claimIds).size !== claimIds.length) errors.push("profileExplanation.claimDecisions contains duplicate claim ids");
|
|
625
|
+
if (new Set(conflictIds).size !== conflictIds.length) errors.push("profileExplanation.conflicts contains duplicate conflict ids");
|
|
626
|
+
if (new Set(needIds).size !== needIds.length) errors.push("profileExplanation.confirmationNeeds contains duplicate need ids");
|
|
627
|
+
const sourceSet = new Set(sourceIds);
|
|
628
|
+
const claimSet = new Set(claimIds);
|
|
629
|
+
for (const source of value.sourceSummaries ?? []) for (const claimId of source.claimIds ?? []) if (!claimSet.has(claimId)) errors.push(`profileExplanation source references unknown claim: ${claimId}`);
|
|
630
|
+
for (const decision of value.claimDecisions ?? []) if (!sourceSet.has(decision.sourceId)) errors.push(`profileExplanation decision references unknown source: ${decision.claimId}`);
|
|
631
|
+
for (const conflict of value.conflicts ?? []) for (const claimId of conflict.claimIds ?? []) if (!claimSet.has(claimId)) errors.push(`profileExplanation conflict references unknown claim: ${claimId}`);
|
|
632
|
+
for (const need of value.confirmationNeeds ?? []) for (const claimId of need.claimIds ?? []) if (!claimSet.has(claimId)) errors.push(`profileExplanation confirmation need references unknown claim: ${claimId}`);
|
|
633
|
+
const unresolved = (value.claimDecisions ?? []).some((decision) => decision.disposition === "unresolved");
|
|
634
|
+
const sourceGap = (value.sourceSummaries ?? []).some((source) => source.status !== "observed");
|
|
635
|
+
if (value.completeness === "complete" && ((value.conflicts?.length ?? 0) > 0 || (value.confirmationNeeds?.length ?? 0) > 0 || (value.limitedReasons?.length ?? 0) > 0 || unresolved || sourceGap)) errors.push("complete profileExplanation cannot contain unresolved, unavailable, conflict, confirmation, or limited state");
|
|
636
|
+
if (["degraded", "unresolved"].includes(value.completeness) && (value.limitedReasons?.length ?? 0) === 0) errors.push("non-complete profileExplanation requires limitedReasons");
|
|
637
|
+
if ((unresolved || (value.conflicts?.length ?? 0) > 0) && value.completeness !== "unresolved") errors.push("unresolved decisions or conflicts require unresolved profileExplanation");
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function validateEntryMode(value, errors, label = "entryMode") {
|
|
641
|
+
const keys = ["schemaVersion", "mode", "sourceEvidenceIds", "observedAt"];
|
|
642
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
643
|
+
contractVersion(value.schemaVersion, "1.0", label, errors);
|
|
644
|
+
if (!["new", "resumed", "imported"].includes(value.mode)) errors.push(`${label}.mode is invalid`);
|
|
645
|
+
strings(value.sourceEvidenceIds, `${label}.sourceEvidenceIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
646
|
+
if (["resumed", "imported"].includes(value.mode) && (value.sourceEvidenceIds?.length ?? 0) === 0) errors.push(`${label} resumed/imported mode requires sourceEvidenceIds`);
|
|
647
|
+
dateTime(value.observedAt, `${label}.observedAt`, errors);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function validatePhaseSliceCorrelation(value, errors, label = "phaseSliceCorrelation") {
|
|
651
|
+
const keys = ["schemaVersion", "correlationId", "taskId", "parentTaskId", "phaseId", "sliceId", "executionAttemptId", "ownerWorkspaceIdentityId", "requiredSliceIds", "aggregateClosureClaim"];
|
|
652
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
653
|
+
contractVersion(value.schemaVersion, "1.0", label, errors);
|
|
654
|
+
string(value.correlationId, `${label}.correlationId`, errors, taskIdPattern);
|
|
655
|
+
string(value.taskId, `${label}.taskId`, errors, taskIdPattern);
|
|
656
|
+
nullableString(value.parentTaskId, `${label}.parentTaskId`, errors, taskIdPattern);
|
|
657
|
+
string(value.phaseId, `${label}.phaseId`, errors, taskIdPattern);
|
|
658
|
+
string(value.sliceId, `${label}.sliceId`, errors, taskIdPattern);
|
|
659
|
+
string(value.executionAttemptId, `${label}.executionAttemptId`, errors, taskIdPattern);
|
|
660
|
+
string(value.ownerWorkspaceIdentityId, `${label}.ownerWorkspaceIdentityId`, errors, taskIdPattern);
|
|
661
|
+
strings(value.requiredSliceIds, `${label}.requiredSliceIds`, errors, { min: 1, pattern: taskIdPattern, unique: true });
|
|
662
|
+
if (!value.requiredSliceIds?.includes(value.sliceId)) errors.push(`${label}.requiredSliceIds must include the exact sliceId`);
|
|
663
|
+
if (value.aggregateClosureClaim !== false) errors.push(`${label}.aggregateClosureClaim must equal false`);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function validateOperationAuthorityRequirement(value, errors) {
|
|
667
|
+
const label = "operationAuthorityRequirement";
|
|
668
|
+
const keys = ["schemaVersion", "requirementId", "workspaceIdentityId", "operation", "target", "subjectId", "scope", "eligibleIssuerSourceIds", "acceptedIssuerEvidenceKinds", "freshness", "declaredAt"];
|
|
669
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
670
|
+
contractVersion(value.schemaVersion, "1.0", label, errors);
|
|
671
|
+
string(value.requirementId, `${label}.requirementId`, errors, taskIdPattern);
|
|
672
|
+
string(value.workspaceIdentityId, `${label}.workspaceIdentityId`, errors, taskIdPattern);
|
|
673
|
+
string(value.operation, `${label}.operation`, errors);
|
|
674
|
+
validateTarget(value.target, `${label}.target`, errors);
|
|
675
|
+
string(value.subjectId, `${label}.subjectId`, errors, taskIdPattern);
|
|
676
|
+
strings(value.scope, `${label}.scope`, errors, { min: 1, unique: true });
|
|
677
|
+
strings(value.eligibleIssuerSourceIds, `${label}.eligibleIssuerSourceIds`, errors, { min: 1, pattern: taskIdPattern, unique: true });
|
|
678
|
+
strings(value.acceptedIssuerEvidenceKinds, `${label}.acceptedIssuerEvidenceKinds`, errors, { min: 1, unique: true });
|
|
679
|
+
if (exactKeys(value.freshness, ["maxAgeSeconds", "expiresRequired"], [], `${label}.freshness`, errors)) {
|
|
680
|
+
if (!Number.isInteger(value.freshness.maxAgeSeconds) || value.freshness.maxAgeSeconds < 1) errors.push(`${label}.freshness.maxAgeSeconds must be a positive integer`);
|
|
681
|
+
if (typeof value.freshness.expiresRequired !== "boolean") errors.push(`${label}.freshness.expiresRequired must be a boolean`);
|
|
682
|
+
}
|
|
683
|
+
dateTime(value.declaredAt, `${label}.declaredAt`, errors);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function validateEvidenceIdentity(value, errors) {
|
|
687
|
+
const label = "evidenceIdentity";
|
|
688
|
+
const keys = ["schemaVersion", "evidenceId", "workspaceIdentityId", "scopeKind", "taskId", "subjectId", "envelopeRevisionId", "controlRevisionId", "source", "contentDigest", "observedAt", "availability", "sanitized", "limitedReason"];
|
|
689
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
690
|
+
contractVersion(value.schemaVersion, "1.0", label, errors);
|
|
691
|
+
string(value.evidenceId, `${label}.evidenceId`, errors, taskIdPattern);
|
|
692
|
+
string(value.workspaceIdentityId, `${label}.workspaceIdentityId`, errors, taskIdPattern);
|
|
693
|
+
if (!["workspace", "task", "subject"].includes(value.scopeKind)) errors.push(`${label}.scopeKind is invalid`);
|
|
694
|
+
nullableString(value.taskId, `${label}.taskId`, errors, taskIdPattern);
|
|
695
|
+
nullableString(value.subjectId, `${label}.subjectId`, errors, taskIdPattern);
|
|
696
|
+
nullableString(value.envelopeRevisionId, `${label}.envelopeRevisionId`, errors, taskIdPattern);
|
|
697
|
+
nullableString(value.controlRevisionId, `${label}.controlRevisionId`, errors, taskIdPattern);
|
|
698
|
+
if (exactKeys(value.source, ["kind", "providerId", "locator", "providerEvidenceId"], [], `${label}.source`, errors)) {
|
|
699
|
+
if (!["file", "git", "test", "log", "provider", "external-receipt", "agent-claim", "other"].includes(value.source.kind)) errors.push(`${label}.source.kind is invalid`);
|
|
700
|
+
nullableString(value.source.providerId, `${label}.source.providerId`, errors, idPattern);
|
|
701
|
+
string(value.source.locator, `${label}.source.locator`, errors);
|
|
702
|
+
nullableString(value.source.providerEvidenceId, `${label}.source.providerEvidenceId`, errors);
|
|
703
|
+
}
|
|
704
|
+
nullableString(value.contentDigest, `${label}.contentDigest`, errors, digestPattern);
|
|
705
|
+
dateTime(value.observedAt, `${label}.observedAt`, errors);
|
|
706
|
+
if (!["available", "unavailable", "unverified"].includes(value.availability)) errors.push(`${label}.availability is invalid`);
|
|
707
|
+
if (value.sanitized !== true) errors.push(`${label}.sanitized must equal true`);
|
|
708
|
+
nullableString(value.limitedReason, `${label}.limitedReason`, errors);
|
|
709
|
+
if (["task", "subject"].includes(value.scopeKind) && (!value.taskId || !value.envelopeRevisionId || !value.controlRevisionId)) errors.push(`${label} task-scoped evidence requires taskId, envelopeRevisionId, and controlRevisionId`);
|
|
710
|
+
if (value.scopeKind === "subject" && !value.subjectId) errors.push(`${label} subject-scoped evidence requires subjectId`);
|
|
711
|
+
if (value.availability !== "available" && !value.limitedReason) errors.push(`${label}.limitedReason is required when evidence is not available`);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function validateControlTaskEnvelope(value, errors) {
|
|
715
|
+
const label = "controlTaskEnvelope";
|
|
716
|
+
const keys = ["schemaVersion", "envelopeId", "revisionId", "taskId", "workspaceIdentityId", "profileId", "profileRevisionId", "subject", "intent", "nonGoals", "entryMode", "phaseSliceCorrelation", "operations", "prohibitedOperations", "requiredGateIds", "returnContract", "createdAt"];
|
|
717
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
718
|
+
contractVersion(value.schemaVersion, "2.0", label, errors);
|
|
719
|
+
for (const key of ["envelopeId", "revisionId", "taskId", "workspaceIdentityId", "profileId", "profileRevisionId"]) string(value[key], `${label}.${key}`, errors, taskIdPattern);
|
|
720
|
+
validateSubject(value.subject, `${label}.subject`, errors);
|
|
721
|
+
string(value.intent, `${label}.intent`, errors);
|
|
722
|
+
strings(value.nonGoals, `${label}.nonGoals`, errors, { unique: true });
|
|
723
|
+
validateEntryMode(value.entryMode, errors, `${label}.entryMode`);
|
|
724
|
+
validatePhaseSliceCorrelation(value.phaseSliceCorrelation, errors, `${label}.phaseSliceCorrelation`);
|
|
725
|
+
if (!Array.isArray(value.operations)) errors.push(`${label}.operations must be an array`);
|
|
726
|
+
else value.operations.forEach((operation, index) => {
|
|
727
|
+
const operationLabel = `${label}.operations[${index}]`;
|
|
728
|
+
if (!exactKeys(operation, ["operationId", "operation", "target", "scope", "authorityRequirementId"], [], operationLabel, errors)) return;
|
|
729
|
+
string(operation.operationId, `${operationLabel}.operationId`, errors, taskIdPattern);
|
|
730
|
+
string(operation.operation, `${operationLabel}.operation`, errors);
|
|
731
|
+
validateTarget(operation.target, `${operationLabel}.target`, errors);
|
|
732
|
+
strings(operation.scope, `${operationLabel}.scope`, errors, { min: 1, unique: true });
|
|
733
|
+
string(operation.authorityRequirementId, `${operationLabel}.authorityRequirementId`, errors, taskIdPattern);
|
|
734
|
+
});
|
|
735
|
+
const operationIds = value.operations?.map((operation) => operation.operationId) ?? [];
|
|
736
|
+
if (new Set(operationIds).size !== operationIds.length) errors.push(`${label}.operations contains duplicate operation ids`);
|
|
737
|
+
strings(value.prohibitedOperations, `${label}.prohibitedOperations`, errors, { unique: true });
|
|
738
|
+
for (const operation of value.operations ?? []) if (value.prohibitedOperations?.includes(operation.operation)) errors.push(`${label} allows and prohibits the same operation: ${operation.operation}`);
|
|
739
|
+
strings(value.requiredGateIds, `${label}.requiredGateIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
740
|
+
if (value.returnContract !== "return-receipt-v2") errors.push(`${label}.returnContract is invalid`);
|
|
741
|
+
dateTime(value.createdAt, `${label}.createdAt`, errors);
|
|
742
|
+
if (value.phaseSliceCorrelation?.taskId !== value.taskId) errors.push(`${label} taskId does not match phase/slice correlation`);
|
|
743
|
+
if (value.phaseSliceCorrelation?.ownerWorkspaceIdentityId !== value.workspaceIdentityId) errors.push(`${label} Workspace Identity does not match phase/slice correlation`);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function validateOperationGrant(value, errors) {
|
|
747
|
+
const label = "operationGrant";
|
|
748
|
+
const keys = ["schemaVersion", "grantId", "grantDigest", "authorityRequirementId", "workspaceIdentityId", "envelopeId", "envelopeRevisionId", "executorId", "operation", "target", "subjectId", "candidateId", "scope", "issuerId", "issuerEvidenceId", "issuedAt", "notBefore", "expiresAt", "status", "invalidatedAt", "invalidationEvidenceId", "supersededByGrantId", "retroactiveEffect"];
|
|
749
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
750
|
+
contractVersion(value.schemaVersion, "1.0", label, errors);
|
|
751
|
+
for (const key of ["grantId", "authorityRequirementId", "workspaceIdentityId", "envelopeId", "envelopeRevisionId", "subjectId", "issuerEvidenceId"]) string(value[key], `${label}.${key}`, errors, taskIdPattern);
|
|
752
|
+
string(value.grantDigest, `${label}.grantDigest`, errors, digestPattern);
|
|
753
|
+
string(value.executorId, `${label}.executorId`, errors);
|
|
754
|
+
string(value.operation, `${label}.operation`, errors);
|
|
755
|
+
validateTarget(value.target, `${label}.target`, errors);
|
|
756
|
+
nullableString(value.candidateId, `${label}.candidateId`, errors, taskIdPattern);
|
|
757
|
+
strings(value.scope, `${label}.scope`, errors, { min: 1, unique: true });
|
|
758
|
+
string(value.issuerId, `${label}.issuerId`, errors);
|
|
759
|
+
dateTime(value.issuedAt, `${label}.issuedAt`, errors);
|
|
760
|
+
dateTime(value.notBefore, `${label}.notBefore`, errors);
|
|
761
|
+
dateTime(value.expiresAt, `${label}.expiresAt`, errors);
|
|
762
|
+
if (!["current", "expired", "revoked", "stale", "superseded"].includes(value.status)) errors.push(`${label}.status is invalid`);
|
|
763
|
+
if (value.invalidatedAt !== null) dateTime(value.invalidatedAt, `${label}.invalidatedAt`, errors);
|
|
764
|
+
nullableString(value.invalidationEvidenceId, `${label}.invalidationEvidenceId`, errors, taskIdPattern);
|
|
765
|
+
nullableString(value.supersededByGrantId, `${label}.supersededByGrantId`, errors, taskIdPattern);
|
|
766
|
+
if (exactKeys(value.retroactiveEffect, ["applies", "scope"], [], `${label}.retroactiveEffect`, errors)) {
|
|
767
|
+
if (typeof value.retroactiveEffect.applies !== "boolean") errors.push(`${label}.retroactiveEffect.applies must be a boolean`);
|
|
768
|
+
strings(value.retroactiveEffect.scope, `${label}.retroactiveEffect.scope`, errors, { unique: true });
|
|
769
|
+
if (value.retroactiveEffect.applies && value.retroactiveEffect.scope.length === 0) errors.push(`${label} bounded retroactive effect requires scope`);
|
|
770
|
+
}
|
|
771
|
+
if (Date.parse(value.notBefore) < Date.parse(value.issuedAt)) errors.push(`${label}.notBefore cannot precede issuedAt`);
|
|
772
|
+
if (Date.parse(value.expiresAt) <= Date.parse(value.notBefore)) errors.push(`${label}.expiresAt must follow notBefore`);
|
|
773
|
+
if (value.status === "current" && (value.invalidatedAt !== null || value.invalidationEvidenceId !== null || value.supersededByGrantId !== null)) errors.push(`${label} current grant cannot carry invalidation or successor identity`);
|
|
774
|
+
if (value.status !== "current" && value.invalidatedAt === null) errors.push(`${label} non-current grant requires invalidatedAt`);
|
|
775
|
+
if (["revoked", "stale", "superseded"].includes(value.status) && !value.invalidationEvidenceId) errors.push(`${label} revoked, stale, or superseded grant requires invalidationEvidenceId`);
|
|
776
|
+
if (value.status === "superseded" && !value.supersededByGrantId) errors.push(`${label} superseded grant requires supersededByGrantId`);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function validateGateResult(value, errors) {
|
|
780
|
+
const label = "gateResult";
|
|
781
|
+
const keys = ["schemaVersion", "gateResultId", "gateId", "gateKind", "workspaceIdentityId", "envelopeRevisionId", "controlRevisionId", "subjectId", "disposition", "evidenceIdentityIds", "evaluatedAt", "limitedReason"];
|
|
782
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
783
|
+
contractVersion(value.schemaVersion, "1.0", label, errors);
|
|
784
|
+
for (const key of ["gateResultId", "gateId", "workspaceIdentityId", "envelopeRevisionId", "controlRevisionId", "subjectId"]) string(value[key], `${label}.${key}`, errors, taskIdPattern);
|
|
785
|
+
if (!["operation-authorization", "review-authority", "validation", "governance-dependency", "receipt-verification"].includes(value.gateKind)) errors.push(`${label}.gateKind is invalid`);
|
|
786
|
+
if (!["satisfied", "unsatisfied", "blocked", "deviation_requires_review"].includes(value.disposition)) errors.push(`${label}.disposition is invalid`);
|
|
787
|
+
strings(value.evidenceIdentityIds, `${label}.evidenceIdentityIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
788
|
+
dateTime(value.evaluatedAt, `${label}.evaluatedAt`, errors);
|
|
789
|
+
nullableString(value.limitedReason, `${label}.limitedReason`, errors);
|
|
790
|
+
if (value.disposition === "satisfied" && (value.evidenceIdentityIds?.length ?? 0) === 0) errors.push(`${label} satisfied result requires evidence`);
|
|
791
|
+
if (value.disposition === "satisfied" && value.limitedReason !== null) errors.push(`${label} satisfied result cannot carry limitedReason`);
|
|
792
|
+
if (value.disposition !== "satisfied" && !value.limitedReason) errors.push(`${label} non-satisfied result requires limitedReason`);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function validateControlRevision(value, errors) {
|
|
796
|
+
const label = "controlRevision";
|
|
797
|
+
const keys = ["schemaVersion", "revisionId", "predecessorRevisionId", "taskId", "workspaceIdentityId", "profileId", "profileRevisionId", "envelopeId", "envelopeRevisionId", "subjectId", "entryMode", "phaseSliceCorrelation", "state", "operationGrantIds", "gateResultIds", "evidenceIdentityIds", "deviationEvidenceIds", "returnReceiptId", "observedAt"];
|
|
798
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
799
|
+
contractVersion(value.schemaVersion, "1.0", label, errors);
|
|
800
|
+
for (const key of ["revisionId", "taskId", "workspaceIdentityId", "profileId", "profileRevisionId", "envelopeId", "envelopeRevisionId", "subjectId"]) string(value[key], `${label}.${key}`, errors, taskIdPattern);
|
|
801
|
+
nullableString(value.predecessorRevisionId, `${label}.predecessorRevisionId`, errors, taskIdPattern);
|
|
802
|
+
if (value.predecessorRevisionId === value.revisionId) errors.push(`${label} cannot be its own predecessor`);
|
|
803
|
+
validateEntryMode(value.entryMode, errors, `${label}.entryMode`);
|
|
804
|
+
validatePhaseSliceCorrelation(value.phaseSliceCorrelation, errors, `${label}.phaseSliceCorrelation`);
|
|
805
|
+
if (!["observed", "profile_resolved", "awaiting_authorization", "ready_for_execution", "execution_finished", "verification_pending", "deviation_requires_review", "blocked", "closed"].includes(value.state)) errors.push(`${label}.state is invalid`);
|
|
806
|
+
for (const key of ["operationGrantIds", "gateResultIds", "evidenceIdentityIds", "deviationEvidenceIds"]) strings(value[key], `${label}.${key}`, errors, { pattern: taskIdPattern, unique: true });
|
|
807
|
+
nullableString(value.returnReceiptId, `${label}.returnReceiptId`, errors, taskIdPattern);
|
|
808
|
+
dateTime(value.observedAt, `${label}.observedAt`, errors);
|
|
809
|
+
if (value.phaseSliceCorrelation?.taskId !== value.taskId) errors.push(`${label} taskId does not match phase/slice correlation`);
|
|
810
|
+
if (value.phaseSliceCorrelation?.ownerWorkspaceIdentityId !== value.workspaceIdentityId) errors.push(`${label} Workspace Identity does not match phase/slice correlation`);
|
|
811
|
+
if (value.state === "closed" && (!value.returnReceiptId || (value.deviationEvidenceIds?.length ?? 0) > 0)) errors.push(`${label} closed state requires a receipt and no unresolved deviations`);
|
|
812
|
+
if (value.state === "deviation_requires_review" && (value.deviationEvidenceIds?.length ?? 0) === 0) errors.push(`${label} deviation state requires evidence`);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function validateReturnReceiptV2(value, errors) {
|
|
816
|
+
const label = "returnReceiptV2";
|
|
817
|
+
const keys = ["schemaVersion", "receiptId", "taskId", "workspaceIdentityId", "profileId", "profileRevisionId", "envelopeId", "envelopeRevisionId", "controlRevisionId", "subjectId", "phaseSliceCorrelation", "exercisedOperationGrantIds", "changedScope", "validationEvidenceIdentityIds", "externalSideEffects", "confirmedNonMutations", "residualRisks", "rollbackEnvelopeLineageId", "deviations", "claimStatus", "verificationGateResultId", "closeout", "claimedAt"];
|
|
818
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
819
|
+
contractVersion(value.schemaVersion, "2.0", label, errors);
|
|
820
|
+
for (const key of ["receiptId", "taskId", "workspaceIdentityId", "profileId", "profileRevisionId", "envelopeId", "envelopeRevisionId", "controlRevisionId", "subjectId"]) string(value[key], `${label}.${key}`, errors, taskIdPattern);
|
|
821
|
+
validatePhaseSliceCorrelation(value.phaseSliceCorrelation, errors, `${label}.phaseSliceCorrelation`);
|
|
822
|
+
strings(value.exercisedOperationGrantIds, `${label}.exercisedOperationGrantIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
823
|
+
strings(value.changedScope, `${label}.changedScope`, errors, { unique: true });
|
|
824
|
+
strings(value.validationEvidenceIdentityIds, `${label}.validationEvidenceIdentityIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
825
|
+
if (!Array.isArray(value.externalSideEffects)) errors.push(`${label}.externalSideEffects must be an array`);
|
|
826
|
+
else value.externalSideEffects.forEach((effect, index) => {
|
|
827
|
+
const effectLabel = `${label}.externalSideEffects[${index}]`;
|
|
828
|
+
if (!exactKeys(effect, ["operationId", "targetLocator", "observedAt"], [], effectLabel, errors)) return;
|
|
829
|
+
string(effect.operationId, `${effectLabel}.operationId`, errors, taskIdPattern);
|
|
830
|
+
string(effect.targetLocator, `${effectLabel}.targetLocator`, errors);
|
|
831
|
+
dateTime(effect.observedAt, `${effectLabel}.observedAt`, errors);
|
|
832
|
+
});
|
|
833
|
+
const sideEffectIds = value.externalSideEffects?.map((effect) => effect.operationId) ?? [];
|
|
834
|
+
if (new Set(sideEffectIds).size !== sideEffectIds.length) errors.push(`${label}.externalSideEffects contains duplicate operation ids`);
|
|
835
|
+
for (const key of ["confirmedNonMutations", "residualRisks", "deviations"]) strings(value[key], `${label}.${key}`, errors, { unique: true });
|
|
836
|
+
nullableString(value.rollbackEnvelopeLineageId, `${label}.rollbackEnvelopeLineageId`, errors, taskIdPattern);
|
|
837
|
+
if (!["claimed", "verified", "rejected", "stale"].includes(value.claimStatus)) errors.push(`${label}.claimStatus is invalid`);
|
|
838
|
+
nullableString(value.verificationGateResultId, `${label}.verificationGateResultId`, errors, taskIdPattern);
|
|
839
|
+
if (!["complete", "incomplete", "blocked"].includes(value.closeout)) errors.push(`${label}.closeout is invalid`);
|
|
840
|
+
dateTime(value.claimedAt, `${label}.claimedAt`, errors);
|
|
841
|
+
if (value.phaseSliceCorrelation?.taskId !== value.taskId) errors.push(`${label} taskId does not match phase/slice correlation`);
|
|
842
|
+
if (value.phaseSliceCorrelation?.ownerWorkspaceIdentityId !== value.workspaceIdentityId) errors.push(`${label} Workspace Identity does not match phase/slice correlation`);
|
|
843
|
+
if (value.claimStatus === "verified" && !value.verificationGateResultId) errors.push(`${label} verified receipt requires verificationGateResultId`);
|
|
844
|
+
if (value.closeout === "complete" && (value.claimStatus !== "verified" || !value.verificationGateResultId || (value.deviations?.length ?? 0) > 0)) errors.push(`${label} complete closeout requires verified status, verification evidence, and no deviations`);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function validateRollbackEnvelopeLineage(value, errors) {
|
|
848
|
+
const label = "rollbackEnvelopeLineage";
|
|
849
|
+
const keys = ["schemaVersion", "lineageId", "workspaceIdentityId", "forwardEnvelope", "rollbackEnvelope", "forwardOperationGrantIds", "rollbackOperationGrantIds", "rollbackOperationIds", "state", "validationGateResultIds", "rollbackReceiptId", "declaredByEvidenceId", "reason", "createdAt"];
|
|
850
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
851
|
+
contractVersion(value.schemaVersion, "1.0", label, errors);
|
|
852
|
+
string(value.lineageId, `${label}.lineageId`, errors, taskIdPattern);
|
|
853
|
+
string(value.workspaceIdentityId, `${label}.workspaceIdentityId`, errors, taskIdPattern);
|
|
854
|
+
validateEnvelopeReference(value.forwardEnvelope, `${label}.forwardEnvelope`, errors);
|
|
855
|
+
validateEnvelopeReference(value.rollbackEnvelope, `${label}.rollbackEnvelope`, errors);
|
|
856
|
+
for (const key of ["forwardOperationGrantIds", "rollbackOperationGrantIds", "rollbackOperationIds", "validationGateResultIds"]) strings(value[key], `${label}.${key}`, errors, { pattern: taskIdPattern, unique: true, min: key === "rollbackOperationIds" ? 1 : 0 });
|
|
857
|
+
if (!["awaiting_authorization", "authorized", "executed", "verified", "blocked"].includes(value.state)) errors.push(`${label}.state is invalid`);
|
|
858
|
+
nullableString(value.rollbackReceiptId, `${label}.rollbackReceiptId`, errors, taskIdPattern);
|
|
859
|
+
string(value.declaredByEvidenceId, `${label}.declaredByEvidenceId`, errors, taskIdPattern);
|
|
860
|
+
string(value.reason, `${label}.reason`, errors);
|
|
861
|
+
dateTime(value.createdAt, `${label}.createdAt`, errors);
|
|
862
|
+
if (value.forwardEnvelope?.envelopeId === value.rollbackEnvelope?.envelopeId && value.forwardEnvelope?.revisionId === value.rollbackEnvelope?.revisionId) errors.push(`${label} rollback must use a new Envelope revision`);
|
|
863
|
+
const reusedGrants = (value.rollbackOperationGrantIds ?? []).filter((grantId) => value.forwardOperationGrantIds?.includes(grantId));
|
|
864
|
+
if (reusedGrants.length > 0) errors.push(`${label} rollback cannot reuse forward Operation Grants: ${reusedGrants.join(", ")}`);
|
|
865
|
+
if (["authorized", "executed", "verified"].includes(value.state) && (value.rollbackOperationGrantIds?.length ?? 0) === 0) errors.push(`${label} authorized or executed rollback requires rollback-specific grants`);
|
|
866
|
+
if (value.state === "verified" && (!value.rollbackReceiptId || (value.validationGateResultIds?.length ?? 0) === 0)) errors.push(`${label} verified rollback requires validation gates and its own receipt`);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function validateReviewAuthorityRequirement(value, errors) {
|
|
870
|
+
const label = "reviewAuthorityRequirement";
|
|
871
|
+
const keys = ["schemaVersion", "requirementId", "workspaceIdentityId", "operation", "authorityClass", "subjectId", "scope", "authoritySourceIds", "acceptedEvidenceKinds", "quorum", "actorConstraints", "ownerCoverage", "freshness", "substitutionPolicy", "waiverPolicy", "declaredAt"];
|
|
872
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
873
|
+
contractVersion(value.schemaVersion, "1.0", label, errors);
|
|
874
|
+
for (const key of ["requirementId", "workspaceIdentityId", "subjectId"]) string(value[key], `${label}.${key}`, errors, taskIdPattern);
|
|
875
|
+
string(value.operation, `${label}.operation`, errors);
|
|
876
|
+
if (!authorityClasses.includes(value.authorityClass)) errors.push(`${label}.authorityClass is invalid`);
|
|
877
|
+
strings(value.scope, `${label}.scope`, errors, { min: 1, unique: true });
|
|
878
|
+
strings(value.authoritySourceIds, `${label}.authoritySourceIds`, errors, { min: 1, pattern: taskIdPattern, unique: true });
|
|
879
|
+
strings(value.acceptedEvidenceKinds, `${label}.acceptedEvidenceKinds`, errors, { min: 1, unique: true });
|
|
880
|
+
if (exactKeys(value.quorum, ["minimumCount", "distinctActors"], [], `${label}.quorum`, errors)) {
|
|
881
|
+
if (!Number.isInteger(value.quorum.minimumCount) || value.quorum.minimumCount < 1) errors.push(`${label}.quorum.minimumCount must be a positive integer`);
|
|
882
|
+
if (typeof value.quorum.distinctActors !== "boolean") errors.push(`${label}.quorum.distinctActors must be a boolean`);
|
|
883
|
+
}
|
|
884
|
+
if (exactKeys(value.actorConstraints, ["eligibleActorSourceIds", "excludedRelations"], [], `${label}.actorConstraints`, errors)) {
|
|
885
|
+
strings(value.actorConstraints.eligibleActorSourceIds, `${label}.actorConstraints.eligibleActorSourceIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
886
|
+
strings(value.actorConstraints.excludedRelations, `${label}.actorConstraints.excludedRelations`, errors, { unique: true });
|
|
887
|
+
for (const relation of value.actorConstraints.excludedRelations ?? []) if (!["subject-author", "executor", "evidence-author"].includes(relation)) errors.push(`${label}.actorConstraints.excludedRelations contains an invalid relation`);
|
|
888
|
+
}
|
|
889
|
+
if (exactKeys(value.ownerCoverage, ["required", "surfaceIds"], [], `${label}.ownerCoverage`, errors)) {
|
|
890
|
+
if (typeof value.ownerCoverage.required !== "boolean") errors.push(`${label}.ownerCoverage.required must be a boolean`);
|
|
891
|
+
strings(value.ownerCoverage.surfaceIds, `${label}.ownerCoverage.surfaceIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
892
|
+
if (value.ownerCoverage.required && value.ownerCoverage.surfaceIds.length === 0) errors.push(`${label} required owner coverage needs at least one surface`);
|
|
893
|
+
}
|
|
894
|
+
if (exactKeys(value.freshness, ["maxAgeSeconds", "expiresRequired"], [], `${label}.freshness`, errors)) {
|
|
895
|
+
if (!Number.isInteger(value.freshness.maxAgeSeconds) || value.freshness.maxAgeSeconds < 1) errors.push(`${label}.freshness.maxAgeSeconds must be a positive integer`);
|
|
896
|
+
if (typeof value.freshness.expiresRequired !== "boolean") errors.push(`${label}.freshness.expiresRequired must be a boolean`);
|
|
897
|
+
}
|
|
898
|
+
if (exactKeys(value.substitutionPolicy, ["mode", "equivalenceEdges"], [], `${label}.substitutionPolicy`, errors)) {
|
|
899
|
+
if (!["none", "bounded-equivalence"].includes(value.substitutionPolicy.mode)) errors.push(`${label}.substitutionPolicy.mode is invalid`);
|
|
900
|
+
if (!Array.isArray(value.substitutionPolicy.equivalenceEdges)) errors.push(`${label}.substitutionPolicy.equivalenceEdges must be an array`);
|
|
901
|
+
else value.substitutionPolicy.equivalenceEdges.forEach((edge, index) => {
|
|
902
|
+
const edgeLabel = `${label}.substitutionPolicy.equivalenceEdges[${index}]`;
|
|
903
|
+
if (!exactKeys(edge, ["edgeId", "fromAuthorityClass", "toRequirementId", "sourceId", "direction", "transitive"], [], edgeLabel, errors)) return;
|
|
904
|
+
string(edge.edgeId, `${edgeLabel}.edgeId`, errors, taskIdPattern);
|
|
905
|
+
if (!authorityClasses.includes(edge.fromAuthorityClass)) errors.push(`${edgeLabel}.fromAuthorityClass is invalid`);
|
|
906
|
+
string(edge.toRequirementId, `${edgeLabel}.toRequirementId`, errors, taskIdPattern);
|
|
907
|
+
string(edge.sourceId, `${edgeLabel}.sourceId`, errors, taskIdPattern);
|
|
908
|
+
if (edge.toRequirementId !== value.requirementId) errors.push(`${edgeLabel} must target the exact authority requirement`);
|
|
909
|
+
if (edge.direction !== "one-way" || edge.transitive !== false) errors.push(`${edgeLabel} equivalence must be one-way and non-transitive`);
|
|
910
|
+
});
|
|
911
|
+
const edgeIds = value.substitutionPolicy.equivalenceEdges?.map((edge) => edge.edgeId) ?? [];
|
|
912
|
+
if (new Set(edgeIds).size !== edgeIds.length) errors.push(`${label}.substitutionPolicy.equivalenceEdges contains duplicate edge ids`);
|
|
913
|
+
if (value.substitutionPolicy.mode === "none" && edgeIds.length > 0) errors.push(`${label} non-substitutable requirement cannot declare equivalence edges`);
|
|
914
|
+
if (value.substitutionPolicy.mode === "bounded-equivalence" && edgeIds.length === 0) errors.push(`${label} bounded equivalence requires an explicit edge`);
|
|
915
|
+
}
|
|
916
|
+
if (exactKeys(value.waiverPolicy, ["allowed", "requiredAuthorityClass"], [], `${label}.waiverPolicy`, errors)) {
|
|
917
|
+
if (typeof value.waiverPolicy.allowed !== "boolean") errors.push(`${label}.waiverPolicy.allowed must be a boolean`);
|
|
918
|
+
if (value.waiverPolicy.allowed && !authorityClasses.includes(value.waiverPolicy.requiredAuthorityClass)) errors.push(`${label} allowed waiver requires an authority class`);
|
|
919
|
+
if (!value.waiverPolicy.allowed && value.waiverPolicy.requiredAuthorityClass !== null) errors.push(`${label} disallowed waiver cannot declare an authority class`);
|
|
920
|
+
}
|
|
921
|
+
dateTime(value.declaredAt, `${label}.declaredAt`, errors);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function validateAuthorityEvidence(value, errors) {
|
|
925
|
+
const label = "authorityEvidence";
|
|
926
|
+
const keys = ["schemaVersion", "authorityEvidenceId", "evidenceIdentityId", "requirementId", "workspaceIdentityId", "operation", "authorityClass", "subjectId", "actorId", "actorRelations", "scope", "coveredSurfaceIds", "lifecycle", "observedAt", "effectiveAt", "expiresAt", "invalidatedAt", "invalidationEvidenceIdentityId", "supersededByAuthorityEvidenceId", "limitedReason"];
|
|
927
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
928
|
+
contractVersion(value.schemaVersion, "1.0", label, errors);
|
|
929
|
+
for (const key of ["authorityEvidenceId", "evidenceIdentityId", "requirementId", "workspaceIdentityId", "subjectId"]) string(value[key], `${label}.${key}`, errors, taskIdPattern);
|
|
930
|
+
string(value.operation, `${label}.operation`, errors);
|
|
931
|
+
if (!authorityClasses.includes(value.authorityClass)) errors.push(`${label}.authorityClass is invalid`);
|
|
932
|
+
string(value.actorId, `${label}.actorId`, errors);
|
|
933
|
+
strings(value.actorRelations, `${label}.actorRelations`, errors, { unique: true });
|
|
934
|
+
for (const relation of value.actorRelations ?? []) if (!["subject-author", "executor", "evidence-author", "surface-owner", "independent-reviewer"].includes(relation)) errors.push(`${label}.actorRelations contains an invalid relation`);
|
|
935
|
+
strings(value.scope, `${label}.scope`, errors, { min: 1, unique: true });
|
|
936
|
+
strings(value.coveredSurfaceIds, `${label}.coveredSurfaceIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
937
|
+
const lifecycleStates = ["current", "stale", "revoked", "dismissed", "expired", "superseded"];
|
|
938
|
+
if (!lifecycleStates.includes(value.lifecycle)) errors.push(`${label}.lifecycle is invalid`);
|
|
939
|
+
dateTime(value.observedAt, `${label}.observedAt`, errors);
|
|
940
|
+
dateTime(value.effectiveAt, `${label}.effectiveAt`, errors);
|
|
941
|
+
if (value.expiresAt !== null) dateTime(value.expiresAt, `${label}.expiresAt`, errors);
|
|
942
|
+
if (value.invalidatedAt !== null) dateTime(value.invalidatedAt, `${label}.invalidatedAt`, errors);
|
|
943
|
+
nullableString(value.invalidationEvidenceIdentityId, `${label}.invalidationEvidenceIdentityId`, errors, taskIdPattern);
|
|
944
|
+
nullableString(value.supersededByAuthorityEvidenceId, `${label}.supersededByAuthorityEvidenceId`, errors, taskIdPattern);
|
|
945
|
+
validateNullableLimitedReason(value.limitedReason, errors, `${label}.limitedReason`);
|
|
946
|
+
if (value.lifecycle === "current" && (value.invalidatedAt !== null || value.invalidationEvidenceIdentityId !== null || value.supersededByAuthorityEvidenceId !== null || value.limitedReason !== null)) errors.push(`${label} current evidence cannot carry invalidation, successor, or limited reason`);
|
|
947
|
+
if (value.lifecycle !== "current" && (value.invalidatedAt === null || value.limitedReason === null)) errors.push(`${label} non-current evidence requires invalidation time and limited reason`);
|
|
948
|
+
if (["stale", "revoked", "dismissed", "superseded"].includes(value.lifecycle) && !value.invalidationEvidenceIdentityId) errors.push(`${label} invalidated evidence requires invalidation evidence identity`);
|
|
949
|
+
if (value.lifecycle === "superseded" && !value.supersededByAuthorityEvidenceId) errors.push(`${label} superseded evidence requires successor identity`);
|
|
950
|
+
const expectedCode = { stale: "stale-evidence", revoked: "revoked-evidence", dismissed: "dismissed-evidence", expired: "expired-evidence", superseded: "superseded-evidence" }[value.lifecycle];
|
|
951
|
+
if (expectedCode && value.limitedReason?.code !== expectedCode) errors.push(`${label} lifecycle must use the matching bounded limited reason code`);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function validateValidationTopology(value, errors) {
|
|
955
|
+
const label = "validationTopology";
|
|
956
|
+
const keys = ["schemaVersion", "topologyId", "topologyRevisionId", "workspaceIdentityId", "profileId", "profileRevisionId", "subjectId", "coverageMode", "changedSurfaces", "validationRequirements", "declaredAt"];
|
|
957
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
958
|
+
contractVersion(value.schemaVersion, "1.0", label, errors);
|
|
959
|
+
for (const key of ["topologyId", "topologyRevisionId", "workspaceIdentityId", "profileId", "profileRevisionId", "subjectId"]) string(value[key], `${label}.${key}`, errors, taskIdPattern);
|
|
960
|
+
if (!["required", "advisory", "undeclared"].includes(value.coverageMode)) errors.push(`${label}.coverageMode is invalid`);
|
|
961
|
+
if (!Array.isArray(value.changedSurfaces)) errors.push(`${label}.changedSurfaces must be an array`);
|
|
962
|
+
else value.changedSurfaces.forEach((surface, index) => {
|
|
963
|
+
const surfaceLabel = `${label}.changedSurfaces[${index}]`;
|
|
964
|
+
if (!exactKeys(surface, ["surfaceId", "locator", "ownerIds", "consumerIds", "validationRequirementIds", "sourceIdentityIds"], [], surfaceLabel, errors)) return;
|
|
965
|
+
string(surface.surfaceId, `${surfaceLabel}.surfaceId`, errors, taskIdPattern);
|
|
966
|
+
string(surface.locator, `${surfaceLabel}.locator`, errors);
|
|
967
|
+
strings(surface.ownerIds, `${surfaceLabel}.ownerIds`, errors, { unique: true });
|
|
968
|
+
strings(surface.consumerIds, `${surfaceLabel}.consumerIds`, errors, { unique: true });
|
|
969
|
+
strings(surface.validationRequirementIds, `${surfaceLabel}.validationRequirementIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
970
|
+
strings(surface.sourceIdentityIds, `${surfaceLabel}.sourceIdentityIds`, errors, { min: 1, pattern: taskIdPattern, unique: true });
|
|
971
|
+
});
|
|
972
|
+
if (!Array.isArray(value.validationRequirements)) errors.push(`${label}.validationRequirements must be an array`);
|
|
973
|
+
else value.validationRequirements.forEach((requirement, index) => {
|
|
974
|
+
const requirementLabel = `${label}.validationRequirements[${index}]`;
|
|
975
|
+
if (!exactKeys(requirement, ["requirementId", "entrypointId", "prerequisiteDependencyEdgeIds", "acceptedTrustClasses", "expectedResults", "evidenceLocators", "selected"], [], requirementLabel, errors)) return;
|
|
976
|
+
string(requirement.requirementId, `${requirementLabel}.requirementId`, errors, taskIdPattern);
|
|
977
|
+
string(requirement.entrypointId, `${requirementLabel}.entrypointId`, errors, taskIdPattern);
|
|
978
|
+
strings(requirement.prerequisiteDependencyEdgeIds, `${requirementLabel}.prerequisiteDependencyEdgeIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
979
|
+
strings(requirement.acceptedTrustClasses, `${requirementLabel}.acceptedTrustClasses`, errors, { min: 1, unique: true });
|
|
980
|
+
for (const trustClass of requirement.acceptedTrustClasses ?? []) if (!trustClasses.includes(trustClass)) errors.push(`${requirementLabel}.acceptedTrustClasses contains an invalid trust class`);
|
|
981
|
+
strings(requirement.expectedResults, `${requirementLabel}.expectedResults`, errors, { min: 1, unique: true });
|
|
982
|
+
for (const result of requirement.expectedResults ?? []) if (!["passed", "not_applicable"].includes(result)) errors.push(`${requirementLabel}.expectedResults contains an invalid result`);
|
|
983
|
+
strings(requirement.evidenceLocators, `${requirementLabel}.evidenceLocators`, errors, { min: 1, unique: true });
|
|
984
|
+
if (typeof requirement.selected !== "boolean") errors.push(`${requirementLabel}.selected must be a boolean`);
|
|
985
|
+
});
|
|
986
|
+
const surfaceIds = value.changedSurfaces?.map((surface) => surface.surfaceId) ?? [];
|
|
987
|
+
const requirementIds = value.validationRequirements?.map((requirement) => requirement.requirementId) ?? [];
|
|
988
|
+
if (new Set(surfaceIds).size !== surfaceIds.length) errors.push(`${label}.changedSurfaces contains duplicate surface ids`);
|
|
989
|
+
if (new Set(requirementIds).size !== requirementIds.length) errors.push(`${label}.validationRequirements contains duplicate requirement ids`);
|
|
990
|
+
const requirementSet = new Set(requirementIds);
|
|
991
|
+
const selectedSet = new Set((value.validationRequirements ?? []).filter((requirement) => requirement.selected).map((requirement) => requirement.requirementId));
|
|
992
|
+
for (const surface of value.changedSurfaces ?? []) {
|
|
993
|
+
for (const requirementId of surface.validationRequirementIds ?? []) if (!requirementSet.has(requirementId)) errors.push(`${label} surface references unknown validation requirement: ${requirementId}`);
|
|
994
|
+
if (value.coverageMode === "required" && !(surface.validationRequirementIds ?? []).some((requirementId) => selectedSet.has(requirementId))) errors.push(`${label} required coverage surface lacks a selected validation requirement: ${surface.surfaceId}`);
|
|
995
|
+
}
|
|
996
|
+
dateTime(value.declaredAt, `${label}.declaredAt`, errors);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function validateValidationResult(value, errors) {
|
|
1000
|
+
const label = "validationResult";
|
|
1001
|
+
const keys = ["schemaVersion", "resultId", "topologyId", "topologyRevisionId", "requirementId", "workspaceIdentityId", "envelopeRevisionId", "controlRevisionId", "subjectId", "executionContextIdentityId", "status", "expectedResult", "trustClass", "evidenceIdentityIds", "waiver", "observedAt", "limitedReason"];
|
|
1002
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
1003
|
+
contractVersion(value.schemaVersion, "1.0", label, errors);
|
|
1004
|
+
for (const key of ["resultId", "topologyId", "topologyRevisionId", "requirementId", "workspaceIdentityId", "envelopeRevisionId", "controlRevisionId", "subjectId", "executionContextIdentityId"]) string(value[key], `${label}.${key}`, errors, taskIdPattern);
|
|
1005
|
+
const statuses = ["passed", "failed", "blocked", "unavailable", "not_applicable", "not_selected", "waived"];
|
|
1006
|
+
if (!statuses.includes(value.status)) errors.push(`${label}.status is invalid`);
|
|
1007
|
+
if (!["passed", "not_applicable"].includes(value.expectedResult)) errors.push(`${label}.expectedResult is invalid`);
|
|
1008
|
+
if (!trustClasses.includes(value.trustClass)) errors.push(`${label}.trustClass is invalid`);
|
|
1009
|
+
strings(value.evidenceIdentityIds, `${label}.evidenceIdentityIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
1010
|
+
if (value.waiver !== null && exactKeys(value.waiver, ["authorityEvidenceId", "requirementId", "subjectId", "scope", "expiresAt"], [], `${label}.waiver`, errors)) {
|
|
1011
|
+
for (const key of ["authorityEvidenceId", "requirementId", "subjectId"]) string(value.waiver[key], `${label}.waiver.${key}`, errors, taskIdPattern);
|
|
1012
|
+
strings(value.waiver.scope, `${label}.waiver.scope`, errors, { min: 1, unique: true });
|
|
1013
|
+
dateTime(value.waiver.expiresAt, `${label}.waiver.expiresAt`, errors);
|
|
1014
|
+
if (value.waiver.requirementId !== value.requirementId || value.waiver.subjectId !== value.subjectId) errors.push(`${label} waiver must bind the exact requirement and subject`);
|
|
1015
|
+
}
|
|
1016
|
+
dateTime(value.observedAt, `${label}.observedAt`, errors);
|
|
1017
|
+
validateNullableLimitedReason(value.limitedReason, errors, `${label}.limitedReason`);
|
|
1018
|
+
if (value.status === "passed" && ((value.evidenceIdentityIds?.length ?? 0) === 0 || value.waiver !== null || value.limitedReason !== null)) errors.push(`${label} passed result requires evidence and cannot carry waiver or limited reason`);
|
|
1019
|
+
if (value.status === "failed" && (value.evidenceIdentityIds?.length ?? 0) === 0) errors.push(`${label} failed result requires observed evidence`);
|
|
1020
|
+
if (value.status === "waived" && (value.waiver === null || (value.evidenceIdentityIds?.length ?? 0) === 0 || value.limitedReason === null)) errors.push(`${label} waived result requires bounded authority, evidence, and limited reason`);
|
|
1021
|
+
if (value.status !== "waived" && value.waiver !== null) errors.push(`${label} non-waived result cannot carry waiver authority`);
|
|
1022
|
+
if (value.status !== "passed" && value.limitedReason === null) errors.push(`${label} non-passed result requires a bounded limited reason`);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function validateExecutionContextIdentity(value, errors) {
|
|
1026
|
+
const label = "executionContextIdentity";
|
|
1027
|
+
const keys = ["schemaVersion", "executionContextIdentityId", "workspaceIdentityId", "subjectId", "entrypoint", "invocationRoot", "executor", "runner", "toolIdentities", "providerIdentities", "dependencies", "observedAt", "sanitized"];
|
|
1028
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
1029
|
+
contractVersion(value.schemaVersion, "1.0", label, errors);
|
|
1030
|
+
for (const key of ["executionContextIdentityId", "workspaceIdentityId", "subjectId"]) string(value[key], `${label}.${key}`, errors, taskIdPattern);
|
|
1031
|
+
if (exactKeys(value.entrypoint, ["entrypointId", "kind", "locator", "digest"], [], `${label}.entrypoint`, errors)) {
|
|
1032
|
+
string(value.entrypoint.entrypointId, `${label}.entrypoint.entrypointId`, errors, taskIdPattern);
|
|
1033
|
+
if (!["documented", "local-command", "hook", "ci", "nightly", "protected-environment", "other"].includes(value.entrypoint.kind)) errors.push(`${label}.entrypoint.kind is invalid`);
|
|
1034
|
+
string(value.entrypoint.locator, `${label}.entrypoint.locator`, errors);
|
|
1035
|
+
nullableString(value.entrypoint.digest, `${label}.entrypoint.digest`, errors, digestPattern);
|
|
1036
|
+
}
|
|
1037
|
+
string(value.invocationRoot, `${label}.invocationRoot`, errors);
|
|
1038
|
+
if (exactKeys(value.executor, ["executorId", "kind"], [], `${label}.executor`, errors)) {
|
|
1039
|
+
string(value.executor.executorId, `${label}.executor.executorId`, errors);
|
|
1040
|
+
if (!["host-agent", "human", "local-process", "provider-runner", "other"].includes(value.executor.kind)) errors.push(`${label}.executor.kind is invalid`);
|
|
1041
|
+
}
|
|
1042
|
+
if (exactKeys(value.runner, ["runnerId", "trustClass"], [], `${label}.runner`, errors)) {
|
|
1043
|
+
string(value.runner.runnerId, `${label}.runner.runnerId`, errors);
|
|
1044
|
+
if (!trustClasses.includes(value.runner.trustClass)) errors.push(`${label}.runner.trustClass is invalid`);
|
|
1045
|
+
}
|
|
1046
|
+
strings(value.toolIdentities, `${label}.toolIdentities`, errors, { unique: true });
|
|
1047
|
+
strings(value.providerIdentities, `${label}.providerIdentities`, errors, { unique: true });
|
|
1048
|
+
if (!Array.isArray(value.dependencies)) errors.push(`${label}.dependencies must be an array`);
|
|
1049
|
+
else value.dependencies.forEach((dependency, index) => {
|
|
1050
|
+
const dependencyLabel = `${label}.dependencies[${index}]`;
|
|
1051
|
+
if (!exactKeys(dependency, ["dependencyId", "kind", "locator", "boundary", "declared", "availability", "evidenceIdentityId", "limitedReason"], [], dependencyLabel, errors)) return;
|
|
1052
|
+
string(dependency.dependencyId, `${dependencyLabel}.dependencyId`, errors, taskIdPattern);
|
|
1053
|
+
if (!["environment-file", "configuration", "credential", "toolchain", "network-service", "provider-state", "other"].includes(dependency.kind)) errors.push(`${dependencyLabel}.kind is invalid`);
|
|
1054
|
+
string(dependency.locator, `${dependencyLabel}.locator`, errors);
|
|
1055
|
+
if (!["workspace-contained", "allowlisted-external", "undeclared-external", "unavailable-external"].includes(dependency.boundary)) errors.push(`${dependencyLabel}.boundary is invalid`);
|
|
1056
|
+
if (typeof dependency.declared !== "boolean") errors.push(`${dependencyLabel}.declared must be a boolean`);
|
|
1057
|
+
if (!["available", "unavailable", "unverified"].includes(dependency.availability)) errors.push(`${dependencyLabel}.availability is invalid`);
|
|
1058
|
+
nullableString(dependency.evidenceIdentityId, `${dependencyLabel}.evidenceIdentityId`, errors, taskIdPattern);
|
|
1059
|
+
validateNullableLimitedReason(dependency.limitedReason, errors, `${dependencyLabel}.limitedReason`);
|
|
1060
|
+
if ((dependency.availability !== "available" || ["undeclared-external", "unavailable-external"].includes(dependency.boundary)) && dependency.limitedReason === null) errors.push(`${dependencyLabel} unavailable or boundary-crossing dependency requires a limited reason`);
|
|
1061
|
+
});
|
|
1062
|
+
const dependencyIds = value.dependencies?.map((dependency) => dependency.dependencyId) ?? [];
|
|
1063
|
+
if (new Set(dependencyIds).size !== dependencyIds.length) errors.push(`${label}.dependencies contains duplicate ids`);
|
|
1064
|
+
dateTime(value.observedAt, `${label}.observedAt`, errors);
|
|
1065
|
+
if (value.sanitized !== true) errors.push(`${label}.sanitized must equal true`);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function validateAdapterCapabilityObservation(value, errors, label, allowedCapabilities) {
|
|
1069
|
+
if (!exactKeys(value, ["capability", "state", "evidenceIdentityIds", "limitedReason"], [], label, errors)) return;
|
|
1070
|
+
if (!allowedCapabilities.includes(value.capability)) errors.push(`${label}.capability is invalid`);
|
|
1071
|
+
if (!adapterSupportStates.includes(value.state)) errors.push(`${label}.state is invalid`);
|
|
1072
|
+
strings(value.evidenceIdentityIds, `${label}.evidenceIdentityIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
1073
|
+
validateNullableLimitedReason(value.limitedReason, errors, `${label}.limitedReason`);
|
|
1074
|
+
if (value.state === "supported" && ((value.evidenceIdentityIds?.length ?? 0) === 0 || value.limitedReason !== null)) errors.push(`${label} supported capability requires evidence and no limited reason`);
|
|
1075
|
+
if (value.state !== "supported" && value.limitedReason === null) errors.push(`${label} non-supported capability requires a bounded limited reason`);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function validateAdapterScope(value, errors, label) {
|
|
1079
|
+
if (!["workspace", "task", "subject"].includes(value.scopeKind)) errors.push(`${label}.scopeKind is invalid`);
|
|
1080
|
+
nullableString(value.taskId, `${label}.taskId`, errors, taskIdPattern);
|
|
1081
|
+
nullableString(value.subjectId, `${label}.subjectId`, errors, taskIdPattern);
|
|
1082
|
+
nullableString(value.controlRevisionId, `${label}.controlRevisionId`, errors, taskIdPattern);
|
|
1083
|
+
if (["task", "subject"].includes(value.scopeKind) && (!value.taskId || !value.controlRevisionId)) errors.push(`${label} task-scoped observation requires taskId and controlRevisionId`);
|
|
1084
|
+
if (value.scopeKind === "subject" && !value.subjectId) errors.push(`${label} subject-scoped observation requires subjectId`);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function validateHostAdapterObservation(value, errors) {
|
|
1088
|
+
const label = "hostAdapterObservation";
|
|
1089
|
+
const keys = ["schemaVersion", "observationId", "adapterId", "adapterVersion", "hostIdentityId", "workspaceIdentityId", "scopeKind", "taskId", "subjectId", "controlRevisionId", "authorizationClaim", "capabilities", "observedAt", "sanitized"];
|
|
1090
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
1091
|
+
contractVersion(value.schemaVersion, "1.0", label, errors);
|
|
1092
|
+
string(value.observationId, `${label}.observationId`, errors, taskIdPattern);
|
|
1093
|
+
string(value.adapterId, `${label}.adapterId`, errors, idPattern);
|
|
1094
|
+
string(value.adapterVersion, `${label}.adapterVersion`, errors);
|
|
1095
|
+
string(value.hostIdentityId, `${label}.hostIdentityId`, errors);
|
|
1096
|
+
string(value.workspaceIdentityId, `${label}.workspaceIdentityId`, errors, taskIdPattern);
|
|
1097
|
+
validateAdapterScope(value, errors, label);
|
|
1098
|
+
if (value.authorizationClaim !== false) errors.push(`${label}.authorizationClaim must equal false`);
|
|
1099
|
+
const capabilities = ["instruction-discovery", "workspace-boundary-discovery", "skill-or-plugin-discovery", "binding", "activation", "mutation", "execution-context", "verification"];
|
|
1100
|
+
if (!Array.isArray(value.capabilities) || value.capabilities.length === 0) errors.push(`${label}.capabilities must be a non-empty array`);
|
|
1101
|
+
else value.capabilities.forEach((capability, index) => validateAdapterCapabilityObservation(capability, errors, `${label}.capabilities[${index}]`, capabilities));
|
|
1102
|
+
const capabilityIds = value.capabilities?.map((capability) => capability.capability) ?? [];
|
|
1103
|
+
if (new Set(capabilityIds).size !== capabilityIds.length) errors.push(`${label}.capabilities contains duplicate capability observations`);
|
|
1104
|
+
dateTime(value.observedAt, `${label}.observedAt`, errors);
|
|
1105
|
+
if (value.sanitized !== true) errors.push(`${label}.sanitized must equal true`);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function validateProviderAdapterObservation(value, errors) {
|
|
1109
|
+
const label = "providerAdapterObservation";
|
|
1110
|
+
const keys = ["schemaVersion", "observationId", "adapterId", "adapterVersion", "providerId", "workspaceIdentityId", "sourceIdentityId", "scopeKind", "taskId", "subjectId", "controlRevisionId", "identity", "authorizationClaim", "capabilities", "observedAt", "sanitized"];
|
|
1111
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
1112
|
+
contractVersion(value.schemaVersion, "1.0", label, errors);
|
|
1113
|
+
string(value.observationId, `${label}.observationId`, errors, taskIdPattern);
|
|
1114
|
+
string(value.adapterId, `${label}.adapterId`, errors, idPattern);
|
|
1115
|
+
string(value.adapterVersion, `${label}.adapterVersion`, errors);
|
|
1116
|
+
string(value.providerId, `${label}.providerId`, errors, idPattern);
|
|
1117
|
+
string(value.workspaceIdentityId, `${label}.workspaceIdentityId`, errors, taskIdPattern);
|
|
1118
|
+
string(value.sourceIdentityId, `${label}.sourceIdentityId`, errors, taskIdPattern);
|
|
1119
|
+
validateAdapterScope(value, errors, label);
|
|
1120
|
+
if (exactKeys(value.identity, ["state", "actorId", "evidenceIdentityIds", "limitedReason"], [], `${label}.identity`, errors)) {
|
|
1121
|
+
if (!["authenticated", "unauthenticated", "unverified", "unavailable", "not-required"].includes(value.identity.state)) errors.push(`${label}.identity.state is invalid`);
|
|
1122
|
+
nullableString(value.identity.actorId, `${label}.identity.actorId`, errors);
|
|
1123
|
+
strings(value.identity.evidenceIdentityIds, `${label}.identity.evidenceIdentityIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
1124
|
+
validateNullableLimitedReason(value.identity.limitedReason, errors, `${label}.identity.limitedReason`);
|
|
1125
|
+
if (value.identity.state === "authenticated" && (!value.identity.actorId || (value.identity.evidenceIdentityIds?.length ?? 0) === 0 || value.identity.limitedReason !== null)) errors.push(`${label} authenticated identity requires actor, evidence, and no limited reason`);
|
|
1126
|
+
if (["unauthenticated", "unverified", "unavailable"].includes(value.identity.state) && (value.identity.actorId !== null || value.identity.limitedReason === null)) errors.push(`${label} unavailable identity cannot claim actor and requires a limited reason`);
|
|
1127
|
+
if (value.identity.state === "not-required" && (value.identity.actorId !== null || value.identity.limitedReason !== null)) errors.push(`${label} identity-not-required cannot claim actor or limited reason`);
|
|
1128
|
+
}
|
|
1129
|
+
if (value.authorizationClaim !== false) errors.push(`${label}.authorizationClaim must equal false`);
|
|
1130
|
+
const allowedCapabilities = ["source-observation", "evidence-observation", "authority-observation", "read", "write", "side-effect", "trust-observation", "freshness-observation", "revocation-observation"];
|
|
1131
|
+
if (!Array.isArray(value.capabilities) || value.capabilities.length === 0) errors.push(`${label}.capabilities must be a non-empty array`);
|
|
1132
|
+
else value.capabilities.forEach((capability, index) => {
|
|
1133
|
+
const capabilityLabel = `${label}.capabilities[${index}]`;
|
|
1134
|
+
if (!exactKeys(capability, ["capability", "state", "requiresIdentity", "sideEffectKinds", "approvalRequirementIds", "evidenceIdentityIds", "limitedReason"], [], capabilityLabel, errors)) return;
|
|
1135
|
+
if (!allowedCapabilities.includes(capability.capability)) errors.push(`${capabilityLabel}.capability is invalid`);
|
|
1136
|
+
if (!adapterSupportStates.includes(capability.state)) errors.push(`${capabilityLabel}.state is invalid`);
|
|
1137
|
+
if (typeof capability.requiresIdentity !== "boolean") errors.push(`${capabilityLabel}.requiresIdentity must be a boolean`);
|
|
1138
|
+
strings(capability.sideEffectKinds, `${capabilityLabel}.sideEffectKinds`, errors, { unique: true });
|
|
1139
|
+
strings(capability.approvalRequirementIds, `${capabilityLabel}.approvalRequirementIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
1140
|
+
strings(capability.evidenceIdentityIds, `${capabilityLabel}.evidenceIdentityIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
1141
|
+
validateNullableLimitedReason(capability.limitedReason, errors, `${capabilityLabel}.limitedReason`);
|
|
1142
|
+
if (capability.state === "supported" && ((capability.evidenceIdentityIds?.length ?? 0) === 0 || capability.limitedReason !== null)) errors.push(`${capabilityLabel} supported capability requires evidence and no limited reason`);
|
|
1143
|
+
if (capability.state !== "supported" && capability.limitedReason === null) errors.push(`${capabilityLabel} non-supported capability requires a bounded limited reason`);
|
|
1144
|
+
if (capability.requiresIdentity && value.identity?.state !== "authenticated" && capability.state === "supported") errors.push(`${capabilityLabel} cannot be supported without authenticated identity`);
|
|
1145
|
+
});
|
|
1146
|
+
const capabilityIds = value.capabilities?.map((capability) => capability.capability) ?? [];
|
|
1147
|
+
if (new Set(capabilityIds).size !== capabilityIds.length) errors.push(`${label}.capabilities contains duplicate capability observations`);
|
|
1148
|
+
dateTime(value.observedAt, `${label}.observedAt`, errors);
|
|
1149
|
+
if (value.sanitized !== true) errors.push(`${label}.sanitized must equal true`);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function sameStringSet(left, right) {
|
|
1153
|
+
return Array.isArray(left) && Array.isArray(right)
|
|
1154
|
+
&& left.length === right.length
|
|
1155
|
+
&& left.every((item) => right.includes(item));
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function validateCrossWorkspacePackComposition(value, errors) {
|
|
1159
|
+
const label = "crossWorkspacePackComposition";
|
|
1160
|
+
const keys = ["schemaVersion", "compositionId", "packId", "packVersion", "coreCompatibility", "kernelInvariants", "workspaceSet", "nodes", "dependencyEdges", "phases", "workspaceReceiptBundle", "declaredAt"];
|
|
1161
|
+
if (!exactKeys(value, keys, [], label, errors)) return;
|
|
1162
|
+
contractVersion(value.schemaVersion, "1.0", label, errors);
|
|
1163
|
+
string(value.compositionId, `${label}.compositionId`, errors, taskIdPattern);
|
|
1164
|
+
if (value.packId !== "forgerail-cross-workspace-orchestration") errors.push(`${label}.packId is invalid`);
|
|
1165
|
+
string(value.packVersion, `${label}.packVersion`, errors);
|
|
1166
|
+
|
|
1167
|
+
const compatibilityKeys = ["workspaceIdentityVersions", "taskEnvelopeVersions", "returnReceiptVersions", "phaseSliceCorrelationVersions", "status", "evidenceIdentityIds"];
|
|
1168
|
+
if (exactKeys(value.coreCompatibility, compatibilityKeys, [], `${label}.coreCompatibility`, errors)) {
|
|
1169
|
+
for (const key of ["workspaceIdentityVersions", "taskEnvelopeVersions", "returnReceiptVersions", "phaseSliceCorrelationVersions"]) strings(value.coreCompatibility[key], `${label}.coreCompatibility.${key}`, errors, { min: 1, unique: true });
|
|
1170
|
+
if (!["compatible", "incompatible", "unverified"].includes(value.coreCompatibility.status)) errors.push(`${label}.coreCompatibility.status is invalid`);
|
|
1171
|
+
strings(value.coreCompatibility.evidenceIdentityIds, `${label}.coreCompatibility.evidenceIdentityIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
1172
|
+
if (value.coreCompatibility.status === "compatible" && value.coreCompatibility.evidenceIdentityIds.length === 0) errors.push(`${label} compatible Core contracts require evidence`);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const invariantKeys = ["stateModelRedefined", "operationGrantMinting", "authoritySubstitution", "waiverBroadening", "freshnessBroadening", "receiptVerificationBypass", "transportAsAcceptance"];
|
|
1176
|
+
if (exactKeys(value.kernelInvariants, invariantKeys, [], `${label}.kernelInvariants`, errors)) {
|
|
1177
|
+
for (const key of invariantKeys) if (value.kernelInvariants[key] !== false) errors.push(`${label}.kernelInvariants.${key} must equal false`);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const workspaceSetLabel = `${label}.workspaceSet`;
|
|
1181
|
+
if (exactKeys(value.workspaceSet, ["workspaceSetId", "ownerWorkspaceIdentityIds", "coordinatorWorkspaceIdentityId", "aggregateWriterId", "relationshipIds", "observedAt"], [], workspaceSetLabel, errors)) {
|
|
1182
|
+
string(value.workspaceSet.workspaceSetId, `${workspaceSetLabel}.workspaceSetId`, errors, taskIdPattern);
|
|
1183
|
+
strings(value.workspaceSet.ownerWorkspaceIdentityIds, `${workspaceSetLabel}.ownerWorkspaceIdentityIds`, errors, { min: 2, pattern: taskIdPattern, unique: true });
|
|
1184
|
+
nullableString(value.workspaceSet.coordinatorWorkspaceIdentityId, `${workspaceSetLabel}.coordinatorWorkspaceIdentityId`, errors, taskIdPattern);
|
|
1185
|
+
string(value.workspaceSet.aggregateWriterId, `${workspaceSetLabel}.aggregateWriterId`, errors, taskIdPattern);
|
|
1186
|
+
strings(value.workspaceSet.relationshipIds, `${workspaceSetLabel}.relationshipIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
1187
|
+
dateTime(value.workspaceSet.observedAt, `${workspaceSetLabel}.observedAt`, errors);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
if (!Array.isArray(value.nodes) || value.nodes.length < 2) errors.push(`${label}.nodes must contain at least two nodes`);
|
|
1191
|
+
else value.nodes.forEach((node, index) => {
|
|
1192
|
+
const nodeLabel = `${label}.nodes[${index}]`;
|
|
1193
|
+
if (!exactKeys(node, ["nodeId", "workspaceIdentityId", "workspaceIdentitySchemaVersion", "writerIdentityId", "coreEnvelope", "phaseSlice", "coreReceipt", "state"], [], nodeLabel, errors)) return;
|
|
1194
|
+
for (const key of ["nodeId", "workspaceIdentityId", "writerIdentityId"]) string(node[key], `${nodeLabel}.${key}`, errors, taskIdPattern);
|
|
1195
|
+
string(node.workspaceIdentitySchemaVersion, `${nodeLabel}.workspaceIdentitySchemaVersion`, errors);
|
|
1196
|
+
if (exactKeys(node.coreEnvelope, ["contract", "schemaVersion", "envelopeId", "envelopeRevisionId", "workspaceIdentityId", "subjectId"], [], `${nodeLabel}.coreEnvelope`, errors)) {
|
|
1197
|
+
if (node.coreEnvelope.contract !== "task-envelope-v2") errors.push(`${nodeLabel}.coreEnvelope.contract is invalid`);
|
|
1198
|
+
string(node.coreEnvelope.schemaVersion, `${nodeLabel}.coreEnvelope.schemaVersion`, errors);
|
|
1199
|
+
for (const key of ["envelopeId", "envelopeRevisionId", "workspaceIdentityId", "subjectId"]) string(node.coreEnvelope[key], `${nodeLabel}.coreEnvelope.${key}`, errors, taskIdPattern);
|
|
1200
|
+
if (node.coreEnvelope.workspaceIdentityId !== node.workspaceIdentityId) errors.push(`${nodeLabel} Core Envelope must bind the node Workspace Identity`);
|
|
1201
|
+
}
|
|
1202
|
+
if (exactKeys(node.phaseSlice, ["contract", "schemaVersion", "correlationId", "phaseId", "sliceId", "executionAttemptId"], [], `${nodeLabel}.phaseSlice`, errors)) {
|
|
1203
|
+
if (node.phaseSlice.contract !== "phase-slice-correlation-v1") errors.push(`${nodeLabel}.phaseSlice.contract is invalid`);
|
|
1204
|
+
string(node.phaseSlice.schemaVersion, `${nodeLabel}.phaseSlice.schemaVersion`, errors);
|
|
1205
|
+
for (const key of ["correlationId", "phaseId", "sliceId", "executionAttemptId"]) string(node.phaseSlice[key], `${nodeLabel}.phaseSlice.${key}`, errors, taskIdPattern);
|
|
1206
|
+
}
|
|
1207
|
+
if (node.coreReceipt !== null) {
|
|
1208
|
+
const receiptLabel = `${nodeLabel}.coreReceipt`;
|
|
1209
|
+
const receiptKeys = ["contract", "schemaVersion", "receiptId", "workspaceIdentityId", "envelopeId", "envelopeRevisionId", "controlRevisionId", "subjectId", "verificationStatus", "verificationGateResultId", "transportStatus", "coordinatorAcceptance", "coordinatorAcceptanceEvidenceIdentityIds", "observedAt"];
|
|
1210
|
+
if (exactKeys(node.coreReceipt, receiptKeys, [], receiptLabel, errors)) {
|
|
1211
|
+
if (node.coreReceipt.contract !== "return-receipt-v2") errors.push(`${receiptLabel}.contract is invalid`);
|
|
1212
|
+
string(node.coreReceipt.schemaVersion, `${receiptLabel}.schemaVersion`, errors);
|
|
1213
|
+
for (const key of ["receiptId", "workspaceIdentityId", "envelopeId", "envelopeRevisionId", "controlRevisionId", "subjectId"]) string(node.coreReceipt[key], `${receiptLabel}.${key}`, errors, taskIdPattern);
|
|
1214
|
+
if (!["pending", "verified", "rejected", "stale"].includes(node.coreReceipt.verificationStatus)) errors.push(`${receiptLabel}.verificationStatus is invalid`);
|
|
1215
|
+
nullableString(node.coreReceipt.verificationGateResultId, `${receiptLabel}.verificationGateResultId`, errors, taskIdPattern);
|
|
1216
|
+
if (!["not_delivered", "delivered", "failed"].includes(node.coreReceipt.transportStatus)) errors.push(`${receiptLabel}.transportStatus is invalid`);
|
|
1217
|
+
if (!["pending", "accepted", "rejected", "stale"].includes(node.coreReceipt.coordinatorAcceptance)) errors.push(`${receiptLabel}.coordinatorAcceptance is invalid`);
|
|
1218
|
+
strings(node.coreReceipt.coordinatorAcceptanceEvidenceIdentityIds, `${receiptLabel}.coordinatorAcceptanceEvidenceIdentityIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
1219
|
+
dateTime(node.coreReceipt.observedAt, `${receiptLabel}.observedAt`, errors);
|
|
1220
|
+
if (node.coreReceipt.workspaceIdentityId !== node.workspaceIdentityId
|
|
1221
|
+
|| node.coreReceipt.envelopeId !== node.coreEnvelope?.envelopeId
|
|
1222
|
+
|| node.coreReceipt.envelopeRevisionId !== node.coreEnvelope?.envelopeRevisionId
|
|
1223
|
+
|| node.coreReceipt.subjectId !== node.coreEnvelope?.subjectId) errors.push(`${receiptLabel} must bind the exact node, Envelope revision, and subject`);
|
|
1224
|
+
if (node.coreReceipt.verificationStatus === "verified" && !node.coreReceipt.verificationGateResultId) errors.push(`${receiptLabel} verified receipt requires a verification gate result`);
|
|
1225
|
+
if (node.coreReceipt.coordinatorAcceptance === "accepted" && (node.coreReceipt.verificationStatus !== "verified" || node.coreReceipt.coordinatorAcceptanceEvidenceIdentityIds.length === 0)) errors.push(`${receiptLabel} coordinator acceptance requires a Core-verified receipt and acceptance evidence`);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
if (!["pending", "ready", "executing", "receipt_pending", "verification_pending", "acceptance_pending", "accepted", "blocked", "failed", "stale"].includes(node.state)) errors.push(`${nodeLabel}.state is invalid`);
|
|
1229
|
+
if (node.state === "accepted" && (node.coreReceipt === null || node.coreReceipt.verificationStatus !== "verified" || node.coreReceipt.coordinatorAcceptance !== "accepted")) errors.push(`${nodeLabel} accepted state requires a Core-verified and coordinator-accepted receipt`);
|
|
1230
|
+
if (node.coreReceipt?.coordinatorAcceptance === "accepted" && node.state !== "accepted") errors.push(`${nodeLabel} coordinator-accepted receipt requires accepted node state`);
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
if (!Array.isArray(value.dependencyEdges)) errors.push(`${label}.dependencyEdges must be an array`);
|
|
1234
|
+
else value.dependencyEdges.forEach((edge, index) => {
|
|
1235
|
+
const edgeLabel = `${label}.dependencyEdges[${index}]`;
|
|
1236
|
+
if (!exactKeys(edge, ["edgeId", "predecessorNodeId", "successorNodeId", "kind", "required", "unlockCondition", "authorityTransfer", "operationGrantTransfer", "receiptSubstitution"], [], edgeLabel, errors)) return;
|
|
1237
|
+
for (const key of ["edgeId", "predecessorNodeId", "successorNodeId"]) string(edge[key], `${edgeLabel}.${key}`, errors, taskIdPattern);
|
|
1238
|
+
if (!["delivery", "validation", "acceptance", "rollback"].includes(edge.kind)) errors.push(`${edgeLabel}.kind is invalid`);
|
|
1239
|
+
if (typeof edge.required !== "boolean") errors.push(`${edgeLabel}.required must be a boolean`);
|
|
1240
|
+
if (edge.unlockCondition !== "core-receipt-verified-and-coordinator-accepted") errors.push(`${edgeLabel}.unlockCondition is invalid`);
|
|
1241
|
+
for (const key of ["authorityTransfer", "operationGrantTransfer", "receiptSubstitution"]) if (edge[key] !== false) errors.push(`${edgeLabel}.${key} must equal false`);
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
if (!Array.isArray(value.phases) || value.phases.length === 0) errors.push(`${label}.phases must be a non-empty array`);
|
|
1245
|
+
else value.phases.forEach((phase, index) => {
|
|
1246
|
+
const phaseLabel = `${label}.phases[${index}]`;
|
|
1247
|
+
const phaseKeys = ["phaseId", "requiredNodeIds", "acceptedNodeIds", "pendingNodeIds", "failedNodeIds", "blockedNodeIds", "staleNodeIds", "state", "aggregateClosureClaim"];
|
|
1248
|
+
if (!exactKeys(phase, phaseKeys, [], phaseLabel, errors)) return;
|
|
1249
|
+
string(phase.phaseId, `${phaseLabel}.phaseId`, errors, taskIdPattern);
|
|
1250
|
+
for (const key of ["requiredNodeIds", "acceptedNodeIds", "pendingNodeIds", "failedNodeIds", "blockedNodeIds", "staleNodeIds"]) strings(phase[key], `${phaseLabel}.${key}`, errors, { min: key === "requiredNodeIds" ? 1 : 0, pattern: taskIdPattern, unique: true });
|
|
1251
|
+
if (!["open", "blocked", "closed", "stale"].includes(phase.state)) errors.push(`${phaseLabel}.state is invalid`);
|
|
1252
|
+
if (typeof phase.aggregateClosureClaim !== "boolean") errors.push(`${phaseLabel}.aggregateClosureClaim must be a boolean`);
|
|
1253
|
+
if (phase.state === "closed" && ((phase.pendingNodeIds?.length ?? 0) > 0 || (phase.failedNodeIds?.length ?? 0) > 0 || (phase.blockedNodeIds?.length ?? 0) > 0 || (phase.staleNodeIds?.length ?? 0) > 0 || phase.aggregateClosureClaim !== true)) errors.push(`${phaseLabel} closed phase requires all nodes accepted and an exact aggregate closure claim`);
|
|
1254
|
+
if (phase.state !== "closed" && phase.aggregateClosureClaim !== false) errors.push(`${phaseLabel} non-closed phase cannot claim aggregate closure`);
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
const bundle = value.workspaceReceiptBundle;
|
|
1258
|
+
const bundleLabel = `${label}.workspaceReceiptBundle`;
|
|
1259
|
+
const bundleKeys = ["bundleId", "bundleRevisionId", "predecessorBundleRevisionId", "bundleDigest", "compositionId", "workspaceSetId", "phaseIds", "nodeReceipts", "dependencyStatus", "pendingNodeIds", "failedNodeIds", "blockedNodeIds", "staleNodeIds", "deviationEvidenceIdentityIds", "nextEligibleNodeIds", "state", "observedAt", "immutable"];
|
|
1260
|
+
if (exactKeys(bundle, bundleKeys, [], bundleLabel, errors)) {
|
|
1261
|
+
for (const key of ["bundleId", "bundleRevisionId", "compositionId", "workspaceSetId"]) string(bundle[key], `${bundleLabel}.${key}`, errors, taskIdPattern);
|
|
1262
|
+
nullableString(bundle.predecessorBundleRevisionId, `${bundleLabel}.predecessorBundleRevisionId`, errors, taskIdPattern);
|
|
1263
|
+
string(bundle.bundleDigest, `${bundleLabel}.bundleDigest`, errors, digestPattern);
|
|
1264
|
+
strings(bundle.phaseIds, `${bundleLabel}.phaseIds`, errors, { min: 1, pattern: taskIdPattern, unique: true });
|
|
1265
|
+
if (!Array.isArray(bundle.nodeReceipts)) errors.push(`${bundleLabel}.nodeReceipts must be an array`);
|
|
1266
|
+
else bundle.nodeReceipts.forEach((receipt, index) => {
|
|
1267
|
+
const receiptLabel = `${bundleLabel}.nodeReceipts[${index}]`;
|
|
1268
|
+
if (!exactKeys(receipt, ["nodeId", "receiptId", "verificationStatus", "coordinatorAcceptance"], [], receiptLabel, errors)) return;
|
|
1269
|
+
string(receipt.nodeId, `${receiptLabel}.nodeId`, errors, taskIdPattern);
|
|
1270
|
+
string(receipt.receiptId, `${receiptLabel}.receiptId`, errors, taskIdPattern);
|
|
1271
|
+
if (receipt.verificationStatus !== "verified" || receipt.coordinatorAcceptance !== "accepted") errors.push(`${receiptLabel} must reference a Core-verified and coordinator-accepted receipt`);
|
|
1272
|
+
});
|
|
1273
|
+
if (!Array.isArray(bundle.dependencyStatus)) errors.push(`${bundleLabel}.dependencyStatus must be an array`);
|
|
1274
|
+
else bundle.dependencyStatus.forEach((status, index) => {
|
|
1275
|
+
const statusLabel = `${bundleLabel}.dependencyStatus[${index}]`;
|
|
1276
|
+
if (!exactKeys(status, ["edgeId", "status", "evidenceIdentityIds"], [], statusLabel, errors)) return;
|
|
1277
|
+
string(status.edgeId, `${statusLabel}.edgeId`, errors, taskIdPattern);
|
|
1278
|
+
if (!["locked", "unlocked", "blocked", "stale"].includes(status.status)) errors.push(`${statusLabel}.status is invalid`);
|
|
1279
|
+
strings(status.evidenceIdentityIds, `${statusLabel}.evidenceIdentityIds`, errors, { pattern: taskIdPattern, unique: true });
|
|
1280
|
+
});
|
|
1281
|
+
for (const key of ["pendingNodeIds", "failedNodeIds", "blockedNodeIds", "staleNodeIds", "deviationEvidenceIdentityIds", "nextEligibleNodeIds"]) strings(bundle[key], `${bundleLabel}.${key}`, errors, { pattern: taskIdPattern, unique: true });
|
|
1282
|
+
if (!["assembling", "review_required", "accepted", "blocked", "stale"].includes(bundle.state)) errors.push(`${bundleLabel}.state is invalid`);
|
|
1283
|
+
dateTime(bundle.observedAt, `${bundleLabel}.observedAt`, errors);
|
|
1284
|
+
if (bundle.immutable !== true) errors.push(`${bundleLabel}.immutable must equal true`);
|
|
1285
|
+
if (bundle.compositionId !== value.compositionId || bundle.workspaceSetId !== value.workspaceSet?.workspaceSetId) errors.push(`${bundleLabel} must bind the exact composition and Workspace Set`);
|
|
1286
|
+
if (bundle.predecessorBundleRevisionId === bundle.bundleRevisionId) errors.push(`${bundleLabel} cannot be its own predecessor`);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
const nodes = value.nodes ?? [];
|
|
1290
|
+
const nodeIds = nodes.map((node) => node.nodeId);
|
|
1291
|
+
const nodeMap = new Map(nodes.map((node) => [node.nodeId, node]));
|
|
1292
|
+
if (new Set(nodeIds).size !== nodeIds.length) errors.push(`${label}.nodes contains duplicate node ids`);
|
|
1293
|
+
const nodeWorkspaceIds = [...new Set(nodes.map((node) => node.workspaceIdentityId))];
|
|
1294
|
+
if (!sameStringSet(nodeWorkspaceIds, value.workspaceSet?.ownerWorkspaceIdentityIds ?? [])) errors.push(`${label} Workspace Set must exactly match node owner Workspace Identities`);
|
|
1295
|
+
|
|
1296
|
+
if (value.coreCompatibility?.status === "compatible") {
|
|
1297
|
+
for (const node of nodes) {
|
|
1298
|
+
if (!value.coreCompatibility.workspaceIdentityVersions.includes(node.workspaceIdentitySchemaVersion)) errors.push(`${label} node uses an unsupported Workspace Identity version: ${node.nodeId}`);
|
|
1299
|
+
if (!value.coreCompatibility.taskEnvelopeVersions.includes(node.coreEnvelope?.schemaVersion)) errors.push(`${label} node uses an unsupported Task Envelope version: ${node.nodeId}`);
|
|
1300
|
+
if (!value.coreCompatibility.phaseSliceCorrelationVersions.includes(node.phaseSlice?.schemaVersion)) errors.push(`${label} node uses an unsupported Phase/Slice version: ${node.nodeId}`);
|
|
1301
|
+
if (node.coreReceipt && !value.coreCompatibility.returnReceiptVersions.includes(node.coreReceipt.schemaVersion)) errors.push(`${label} node uses an unsupported Return Receipt version: ${node.nodeId}`);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
if (nodes.some((node) => node.state === "accepted") && value.coreCompatibility?.status !== "compatible") errors.push(`${label} accepted nodes require compatible Core contract versions`);
|
|
1305
|
+
|
|
1306
|
+
const edges = value.dependencyEdges ?? [];
|
|
1307
|
+
const edgeIds = edges.map((edge) => edge.edgeId);
|
|
1308
|
+
if (new Set(edgeIds).size !== edgeIds.length) errors.push(`${label}.dependencyEdges contains duplicate edge ids`);
|
|
1309
|
+
for (const edge of edges) {
|
|
1310
|
+
if (!nodeMap.has(edge.predecessorNodeId) || !nodeMap.has(edge.successorNodeId)) errors.push(`${label} dependency edge references an unknown node: ${edge.edgeId}`);
|
|
1311
|
+
if (edge.predecessorNodeId === edge.successorNodeId) errors.push(`${label} dependency edge cannot be self-referential: ${edge.edgeId}`);
|
|
1312
|
+
}
|
|
1313
|
+
const predecessors = new Map(nodeIds.map((nodeId) => [nodeId, []]));
|
|
1314
|
+
for (const edge of edges) predecessors.get(edge.successorNodeId)?.push(edge.predecessorNodeId);
|
|
1315
|
+
const dependsOn = (nodeId, targetId, seen = new Set()) => {
|
|
1316
|
+
if (seen.has(nodeId)) return false;
|
|
1317
|
+
seen.add(nodeId);
|
|
1318
|
+
const direct = predecessors.get(nodeId) ?? [];
|
|
1319
|
+
return direct.includes(targetId) || direct.some((predecessor) => dependsOn(predecessor, targetId, seen));
|
|
1320
|
+
};
|
|
1321
|
+
for (const nodeId of nodeIds) if (dependsOn(nodeId, nodeId)) errors.push(`${label} dependency graph contains a cycle at node: ${nodeId}`);
|
|
1322
|
+
const writers = new Map();
|
|
1323
|
+
for (const node of nodes) {
|
|
1324
|
+
const prior = writers.get(node.writerIdentityId) ?? [];
|
|
1325
|
+
for (const priorNodeId of prior) if (!dependsOn(node.nodeId, priorNodeId) && !dependsOn(priorNodeId, node.nodeId)) errors.push(`${label} concurrent nodes cannot share writer identity: ${node.writerIdentityId}`);
|
|
1326
|
+
prior.push(node.nodeId);
|
|
1327
|
+
writers.set(node.writerIdentityId, prior);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
const phases = value.phases ?? [];
|
|
1331
|
+
const phaseIds = phases.map((phase) => phase.phaseId);
|
|
1332
|
+
if (new Set(phaseIds).size !== phaseIds.length) errors.push(`${label}.phases contains duplicate phase ids`);
|
|
1333
|
+
for (const phase of phases) {
|
|
1334
|
+
const partitions = [phase.acceptedNodeIds ?? [], phase.pendingNodeIds ?? [], phase.failedNodeIds ?? [], phase.blockedNodeIds ?? [], phase.staleNodeIds ?? []];
|
|
1335
|
+
const partitioned = partitions.flat();
|
|
1336
|
+
if (new Set(partitioned).size !== partitioned.length) errors.push(`${label} phase partitions overlap: ${phase.phaseId}`);
|
|
1337
|
+
if (!sameStringSet(partitioned, phase.requiredNodeIds ?? [])) errors.push(`${label} phase partitions must exactly cover required nodes: ${phase.phaseId}`);
|
|
1338
|
+
for (const nodeId of phase.requiredNodeIds ?? []) {
|
|
1339
|
+
const node = nodeMap.get(nodeId);
|
|
1340
|
+
if (!node) errors.push(`${label} phase references unknown node: ${nodeId}`);
|
|
1341
|
+
else if (node.phaseSlice?.phaseId !== phase.phaseId) errors.push(`${label} node phase identity does not match aggregation: ${nodeId}`);
|
|
1342
|
+
}
|
|
1343
|
+
const expectedAccepted = (phase.requiredNodeIds ?? []).filter((nodeId) => nodeMap.get(nodeId)?.state === "accepted");
|
|
1344
|
+
if (!sameStringSet(expectedAccepted, phase.acceptedNodeIds ?? [])) errors.push(`${label} phase accepted nodes must match accepted Core receipts: ${phase.phaseId}`);
|
|
1345
|
+
const expectedFailed = (phase.requiredNodeIds ?? []).filter((nodeId) => nodeMap.get(nodeId)?.state === "failed");
|
|
1346
|
+
if (!sameStringSet(expectedFailed, phase.failedNodeIds ?? [])) errors.push(`${label} phase failed nodes must match node state: ${phase.phaseId}`);
|
|
1347
|
+
const expectedBlocked = (phase.requiredNodeIds ?? []).filter((nodeId) => nodeMap.get(nodeId)?.state === "blocked");
|
|
1348
|
+
if (!sameStringSet(expectedBlocked, phase.blockedNodeIds ?? [])) errors.push(`${label} phase blocked nodes must match node state: ${phase.phaseId}`);
|
|
1349
|
+
const expectedStale = (phase.requiredNodeIds ?? []).filter((nodeId) => nodeMap.get(nodeId)?.state === "stale");
|
|
1350
|
+
if (!sameStringSet(expectedStale, phase.staleNodeIds ?? [])) errors.push(`${label} phase stale nodes must match node state: ${phase.phaseId}`);
|
|
1351
|
+
const expectedPending = (phase.requiredNodeIds ?? []).filter((nodeId) => !["accepted", "failed", "blocked", "stale"].includes(nodeMap.get(nodeId)?.state));
|
|
1352
|
+
if (!sameStringSet(expectedPending, phase.pendingNodeIds ?? [])) errors.push(`${label} phase pending nodes must match unresolved node state: ${phase.phaseId}`);
|
|
1353
|
+
}
|
|
1354
|
+
for (const node of nodes) if (!phases.some((phase) => phase.requiredNodeIds?.includes(node.nodeId))) errors.push(`${label} node is missing from phase aggregation: ${node.nodeId}`);
|
|
1355
|
+
|
|
1356
|
+
if (bundle) {
|
|
1357
|
+
if (!sameStringSet(bundle.phaseIds ?? [], phaseIds)) errors.push(`${bundleLabel}.phaseIds must exactly match the composition phases`);
|
|
1358
|
+
const bundleNodeSets = [bundle.pendingNodeIds ?? [], bundle.failedNodeIds ?? [], bundle.blockedNodeIds ?? [], bundle.staleNodeIds ?? []];
|
|
1359
|
+
const bundleNonAccepted = bundleNodeSets.flat();
|
|
1360
|
+
if (new Set(bundleNonAccepted).size !== bundleNonAccepted.length) errors.push(`${bundleLabel} node state lists overlap`);
|
|
1361
|
+
for (const nodeId of [...bundleNonAccepted, ...(bundle.nextEligibleNodeIds ?? [])]) if (!nodeMap.has(nodeId)) errors.push(`${bundleLabel} references unknown node: ${nodeId}`);
|
|
1362
|
+
const receiptNodeIds = bundle.nodeReceipts?.map((receipt) => receipt.nodeId) ?? [];
|
|
1363
|
+
if (new Set(receiptNodeIds).size !== receiptNodeIds.length) errors.push(`${bundleLabel}.nodeReceipts contains duplicate node ids`);
|
|
1364
|
+
const acceptedNodeIds = nodes.filter((node) => node.state === "accepted").map((node) => node.nodeId);
|
|
1365
|
+
const failedNodeIds = nodes.filter((node) => node.state === "failed").map((node) => node.nodeId);
|
|
1366
|
+
const blockedNodeIds = nodes.filter((node) => node.state === "blocked").map((node) => node.nodeId);
|
|
1367
|
+
const staleNodeIds = nodes.filter((node) => node.state === "stale").map((node) => node.nodeId);
|
|
1368
|
+
const pendingNodeIds = nodes.filter((node) => !["accepted", "failed", "blocked", "stale"].includes(node.state)).map((node) => node.nodeId);
|
|
1369
|
+
if (!sameStringSet(bundle.failedNodeIds ?? [], failedNodeIds)) errors.push(`${bundleLabel}.failedNodeIds must match node state`);
|
|
1370
|
+
if (!sameStringSet(bundle.blockedNodeIds ?? [], blockedNodeIds)) errors.push(`${bundleLabel}.blockedNodeIds must match node state`);
|
|
1371
|
+
if (!sameStringSet(bundle.staleNodeIds ?? [], staleNodeIds)) errors.push(`${bundleLabel}.staleNodeIds must match node state`);
|
|
1372
|
+
if (!sameStringSet(bundle.pendingNodeIds ?? [], pendingNodeIds)) errors.push(`${bundleLabel}.pendingNodeIds must match unresolved node state`);
|
|
1373
|
+
if (!sameStringSet(receiptNodeIds, acceptedNodeIds)) errors.push(`${bundleLabel}.nodeReceipts must exactly preserve accepted node receipts`);
|
|
1374
|
+
for (const receipt of bundle.nodeReceipts ?? []) {
|
|
1375
|
+
const node = nodeMap.get(receipt.nodeId);
|
|
1376
|
+
if (!node || node.coreReceipt?.receiptId !== receipt.receiptId || node.coreReceipt?.verificationStatus !== "verified" || node.coreReceipt?.coordinatorAcceptance !== "accepted") errors.push(`${bundleLabel} node receipt does not match a Core-verified accepted node: ${receipt.nodeId}`);
|
|
1377
|
+
}
|
|
1378
|
+
const bundleEdgeIds = bundle.dependencyStatus?.map((status) => status.edgeId) ?? [];
|
|
1379
|
+
if (!sameStringSet(bundleEdgeIds, edgeIds)) errors.push(`${bundleLabel}.dependencyStatus must exactly cover dependency edges`);
|
|
1380
|
+
for (const status of bundle.dependencyStatus ?? []) {
|
|
1381
|
+
const edge = edges.find((candidate) => candidate.edgeId === status.edgeId);
|
|
1382
|
+
if (!edge) continue;
|
|
1383
|
+
const predecessorAccepted = nodeMap.get(edge.predecessorNodeId)?.state === "accepted";
|
|
1384
|
+
if (status.status === "unlocked" && (!predecessorAccepted || status.evidenceIdentityIds.length === 0)) errors.push(`${bundleLabel} dependency cannot unlock without an accepted predecessor and evidence: ${status.edgeId}`);
|
|
1385
|
+
if (edge.required && predecessorAccepted && status.status !== "unlocked") errors.push(`${bundleLabel} accepted predecessor must unlock the required dependency: ${status.edgeId}`);
|
|
1386
|
+
}
|
|
1387
|
+
if (bundle.state === "accepted") {
|
|
1388
|
+
if (value.coreCompatibility?.status !== "compatible"
|
|
1389
|
+
|| acceptedNodeIds.length !== nodes.length
|
|
1390
|
+
|| phases.some((phase) => phase.state !== "closed")
|
|
1391
|
+
|| (bundle.pendingNodeIds?.length ?? 0) > 0
|
|
1392
|
+
|| (bundle.failedNodeIds?.length ?? 0) > 0
|
|
1393
|
+
|| (bundle.blockedNodeIds?.length ?? 0) > 0
|
|
1394
|
+
|| (bundle.staleNodeIds?.length ?? 0) > 0
|
|
1395
|
+
|| (bundle.deviationEvidenceIdentityIds?.length ?? 0) > 0) errors.push(`${bundleLabel} accepted state requires compatible Core contracts, all phases closed, all nodes accepted, and no unresolved state`);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
dateTime(value.declaredAt, `${label}.declaredAt`, errors);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
307
1401
|
export function validateContract(type, payload) {
|
|
308
1402
|
const errors = [];
|
|
309
1403
|
if (!contractTypes.includes(type)) return { valid: false, errors: [`unknown contract type: ${type}`] };
|
|
310
|
-
({
|
|
1404
|
+
({
|
|
1405
|
+
pack: validatePack,
|
|
1406
|
+
profile: validateProfile,
|
|
1407
|
+
"profile-candidate": validateProfileCandidate,
|
|
1408
|
+
envelope: validateEnvelope,
|
|
1409
|
+
launch: validateLaunch,
|
|
1410
|
+
receipt: validateReceipt,
|
|
1411
|
+
"host-adapter": validateHostAdapter,
|
|
1412
|
+
"adoption-plan": validateAdoptionPlan,
|
|
1413
|
+
"binding-receipt": validateBindingReceipt,
|
|
1414
|
+
"cross-workspace-pack-composition": validateCrossWorkspacePackComposition,
|
|
1415
|
+
"workspace-identity": validateWorkspaceIdentity,
|
|
1416
|
+
"workspace-relationship": validateWorkspaceRelationship,
|
|
1417
|
+
"limited-reason": validateLimitedReason,
|
|
1418
|
+
"governance-source": validateGovernanceSource,
|
|
1419
|
+
"source-dependency-edge": validateSourceDependencyEdge,
|
|
1420
|
+
"rule-claim": validateRuleClaim,
|
|
1421
|
+
"effective-profile-v2": validateEffectiveProfileV2,
|
|
1422
|
+
"profile-explanation": validateProfileExplanation,
|
|
1423
|
+
"entry-mode": validateEntryMode,
|
|
1424
|
+
"phase-slice-correlation": validatePhaseSliceCorrelation,
|
|
1425
|
+
"operation-authority-requirement": validateOperationAuthorityRequirement,
|
|
1426
|
+
"evidence-identity": validateEvidenceIdentity,
|
|
1427
|
+
"task-envelope-v2": validateControlTaskEnvelope,
|
|
1428
|
+
"operation-grant": validateOperationGrant,
|
|
1429
|
+
"gate-result": validateGateResult,
|
|
1430
|
+
"task-control-revision": validateControlRevision,
|
|
1431
|
+
"return-receipt-v2": validateReturnReceiptV2,
|
|
1432
|
+
"rollback-envelope-lineage": validateRollbackEnvelopeLineage,
|
|
1433
|
+
"review-authority-requirement": validateReviewAuthorityRequirement,
|
|
1434
|
+
"authority-evidence": validateAuthorityEvidence,
|
|
1435
|
+
"validation-topology": validateValidationTopology,
|
|
1436
|
+
"validation-result": validateValidationResult,
|
|
1437
|
+
"execution-context-identity": validateExecutionContextIdentity,
|
|
1438
|
+
"host-adapter-observation": validateHostAdapterObservation,
|
|
1439
|
+
"provider-adapter-observation": validateProviderAdapterObservation
|
|
1440
|
+
})[type](payload, errors);
|
|
311
1441
|
return { valid: errors.length === 0, errors };
|
|
312
1442
|
}
|
|
313
1443
|
|