archctx-contracts 0.5.10 → 0.5.12
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/fixtures/valid/archctx-capabilities.json +2 -2
- package/fixtures/valid/product-version-manifest.json +5 -5
- package/package.json +1 -1
- package/schemas/runtime/archctx-capabilities.schema.json +1 -1
- package/schemas/runtime/changeset.schema.json +208 -35
- package/schemas/runtime/projection-apply-readback.schema.json +1064 -0
- package/src/architecture.ts +79 -0
- package/src/ports.ts +89 -1
- package/src/product-version.ts +1 -1
- package/src/projection.ts +107 -0
package/src/architecture.ts
CHANGED
|
@@ -71,3 +71,82 @@ export interface ArchitectureNotApplicableFlowV1 extends ArchitectureFlowBaseV1
|
|
|
71
71
|
applicability: "not-applicable";
|
|
72
72
|
rationale: string;
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A `forbid-dependency` constraint (`archcontext.constraint/v1`), narrowed to the fields the
|
|
77
|
+
* dependency gate evaluates. A file owned by a `scope.nodes` node (or a descendant) must not
|
|
78
|
+
* import a file owned by a `rule.targets` node (or a descendant). v1 has no `allowedVia` escape.
|
|
79
|
+
*/
|
|
80
|
+
export interface DependencyConstraintV1 {
|
|
81
|
+
id: string;
|
|
82
|
+
severity: "error" | "warning";
|
|
83
|
+
scope: { nodes: string[] };
|
|
84
|
+
rule: { type: "forbid-dependency"; targets: string[] };
|
|
85
|
+
rationale: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const DEPENDENCY_CONSTRAINT_STATUSES = ["not-applicable", "pass", "undetermined", "violated"] as const;
|
|
89
|
+
export type DependencyConstraintStatus = (typeof DEPENDENCY_CONSTRAINT_STATUSES)[number];
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Why a dependency result is not a complete observation: no index answered, the index attested
|
|
93
|
+
* to a different worktree, the import dump hit its limit, a constrained file carries a
|
|
94
|
+
* repository-local import that resolved to no file, or the workspace package map could not be
|
|
95
|
+
* read, so no bare workspace specifier could be resolved.
|
|
96
|
+
*/
|
|
97
|
+
export const DEPENDENCY_CONSTRAINT_REASON_CODES = [
|
|
98
|
+
"code-facts-stale",
|
|
99
|
+
"code-facts-truncated",
|
|
100
|
+
"code-facts-unavailable",
|
|
101
|
+
"unresolved-import",
|
|
102
|
+
"workspace-resolution-failed"
|
|
103
|
+
] as const;
|
|
104
|
+
export type DependencyConstraintReasonCode = (typeof DEPENDENCY_CONSTRAINT_REASON_CODES)[number];
|
|
105
|
+
|
|
106
|
+
export interface DependencyConstraintViolationV1 {
|
|
107
|
+
constraintId: string;
|
|
108
|
+
fromPath: string;
|
|
109
|
+
toPath: string;
|
|
110
|
+
fromNode: string;
|
|
111
|
+
toNode: string;
|
|
112
|
+
severity: "error" | "warning";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface DependencyConstraintEvaluationV1 {
|
|
116
|
+
schemaVersion: "archcontext.dependency-constraint-evaluation/v1";
|
|
117
|
+
status: DependencyConstraintStatus;
|
|
118
|
+
/** `complete` only when an index attested to the evaluated worktree and was not truncated. */
|
|
119
|
+
coverage: "complete" | "partial" | "unknown";
|
|
120
|
+
reasonCodes: DependencyConstraintReasonCode[];
|
|
121
|
+
constraintIds: string[];
|
|
122
|
+
/** Import observations the evaluation read; zero when the index did not attest to this tree. */
|
|
123
|
+
importEdgeCount: number;
|
|
124
|
+
/** Repository-local specifiers from constrained files that resolved to no file. */
|
|
125
|
+
unresolvedImports: { from: string; specifier: string }[];
|
|
126
|
+
violations: DependencyConstraintViolationV1[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The closed `failOn` vocabulary of `.archcontext/policies/review.yaml`. A review finding in one
|
|
131
|
+
* of these categories blocks only when its category is listed; findings outside the vocabulary
|
|
132
|
+
* are never affected by the policy.
|
|
133
|
+
*/
|
|
134
|
+
export const REVIEW_FAIL_ON_CATEGORIES = [
|
|
135
|
+
"incomplete-intervention",
|
|
136
|
+
"invalid-schema",
|
|
137
|
+
"prohibited-dependency",
|
|
138
|
+
"stale-context",
|
|
139
|
+
"unjustified-compatibility"
|
|
140
|
+
] as const;
|
|
141
|
+
export type ReviewFailOnCategory = (typeof REVIEW_FAIL_ON_CATEGORIES)[number];
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The effective review policy. An error in a category missing from `failOn` is downgraded to a
|
|
145
|
+
* warning, with one exception: the task-snapshot HEAD-mismatch `stale-context` finding always stays
|
|
146
|
+
* an error, because a stale snapshot skips every other review gate.
|
|
147
|
+
*/
|
|
148
|
+
export interface ReviewPolicyV1 {
|
|
149
|
+
failOn: ReviewFailOnCategory[];
|
|
150
|
+
/** `default` when the policy file is missing or invalid in any way: every category blocks. */
|
|
151
|
+
source: "policy-file" | "default";
|
|
152
|
+
}
|
package/src/ports.ts
CHANGED
|
@@ -140,13 +140,38 @@ export interface LocalStorePort {
|
|
|
140
140
|
saveReviewResult(reviewId: string, result: unknown): Promise<void>;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
export interface ModelValidationResult {
|
|
144
|
+
valid: boolean;
|
|
145
|
+
errors: string[];
|
|
146
|
+
/**
|
|
147
|
+
* The subset of `errors` that are cross-file references resolving to nothing, such as an ADR
|
|
148
|
+
* `appliesTo` id with no node. Omitted when empty.
|
|
149
|
+
*/
|
|
150
|
+
referenceErrors?: string[];
|
|
151
|
+
/** Problems that do not invalidate the model, such as an ignored legacy setting. Omitted when empty. */
|
|
152
|
+
warnings?: string[];
|
|
153
|
+
modelDigest: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
143
156
|
export interface ModelStorePort {
|
|
144
157
|
loadManifest(workspace: WorkspaceRef): Promise<unknown>;
|
|
145
158
|
loadModel(workspace: WorkspaceRef): Promise<unknown[]>;
|
|
146
|
-
validateModel(workspace: WorkspaceRef): Promise<
|
|
159
|
+
validateModel(workspace: WorkspaceRef): Promise<ModelValidationResult>;
|
|
147
160
|
writeChangeSetPreview(changeSet: unknown): Promise<{ digest: string; summary: string }>;
|
|
148
161
|
}
|
|
149
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Errors that make a model unusable as the base of a write. Dangling references are excluded so
|
|
165
|
+
* that a change repairing them (for example restoring a deleted node) is not blocked; the
|
|
166
|
+
* after-apply model is still validated in full.
|
|
167
|
+
*/
|
|
168
|
+
export function baseModelBlockingErrors(result: ModelValidationResult): string[] {
|
|
169
|
+
if (result.valid) return [];
|
|
170
|
+
if (result.errors.length === 0) return ["unknown validation error"];
|
|
171
|
+
const references = new Set(result.referenceErrors ?? []);
|
|
172
|
+
return result.errors.filter((error) => !references.has(error));
|
|
173
|
+
}
|
|
174
|
+
|
|
150
175
|
export interface PolicyPort {
|
|
151
176
|
evaluateChangeSet(changeSet: unknown): Promise<{ allowed: boolean; violations: string[] }>;
|
|
152
177
|
evaluateReview(input: unknown): Promise<{ result: "pass" | "fail"; findings: unknown[] }>;
|
|
@@ -750,3 +775,66 @@ export interface ChatGptGaToolContract {
|
|
|
750
775
|
requiresLocalConfirmationForWrite: boolean;
|
|
751
776
|
disclosure: string;
|
|
752
777
|
}
|
|
778
|
+
|
|
779
|
+
export type DetachedReviewWorktreeReason =
|
|
780
|
+
| "HEAD_UNAVAILABLE"
|
|
781
|
+
| "HEAD_SHA_MISMATCH"
|
|
782
|
+
| "TREE_OID_MISMATCH"
|
|
783
|
+
| "WORKTREE_NOT_DETACHED"
|
|
784
|
+
| "WORKTREE_NOT_CLEAN";
|
|
785
|
+
|
|
786
|
+
export interface DetachedReviewWorktreeVerification {
|
|
787
|
+
schemaVersion: "archcontext.detached-review-worktree-verification/v1";
|
|
788
|
+
accepted: boolean;
|
|
789
|
+
reasonCode?: DetachedReviewWorktreeReason;
|
|
790
|
+
expected: {
|
|
791
|
+
headSha: string;
|
|
792
|
+
headTreeOid?: string;
|
|
793
|
+
};
|
|
794
|
+
observed: {
|
|
795
|
+
headSha?: string;
|
|
796
|
+
headTreeOid?: string;
|
|
797
|
+
detached?: boolean;
|
|
798
|
+
clean?: boolean;
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
export interface ReviewCheckoutGitPort {
|
|
804
|
+
findRepositoryRoot(start: string): string;
|
|
805
|
+
readOriginUrl(root: string): string | null;
|
|
806
|
+
verifyDetachedWorktree(input: {
|
|
807
|
+
worktreeRoot: string;
|
|
808
|
+
expectedHeadSha: string;
|
|
809
|
+
expectedHeadTreeOid?: string;
|
|
810
|
+
}): DetachedReviewWorktreeVerification;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
export interface NonLocalEgressChannel {
|
|
814
|
+
channel: "context7" | "agent-audit" | "github-issue-publishing" | "npm-update-check" | "codegraph-telemetry";
|
|
815
|
+
destination: string;
|
|
816
|
+
trigger: string;
|
|
817
|
+
data: string;
|
|
818
|
+
status: "enabled" | "declared-awaiting-user-consent" | "blocked-by-policy";
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
export interface LocalEgressReport {
|
|
822
|
+
ok: boolean;
|
|
823
|
+
policyMode: "configured" | "local-only";
|
|
824
|
+
enforcement: "application-admission";
|
|
825
|
+
defaultOutbound: "local-only";
|
|
826
|
+
effectiveOutbound: "local-only" | "non-local";
|
|
827
|
+
nonLocalEgress: NonLocalEgressChannel[];
|
|
828
|
+
cloudContentUpload: "deny";
|
|
829
|
+
secureMcpTunnel: "disabled-by-default";
|
|
830
|
+
thirdPartyTelemetry: "disabled" | "not-disabled-by-env";
|
|
831
|
+
codeGraph: {
|
|
832
|
+
provider: "codegraph";
|
|
833
|
+
telemetry: "disabled" | "not-disabled-by-env";
|
|
834
|
+
envVar: string;
|
|
835
|
+
configuredValue: string | null;
|
|
836
|
+
effectiveValue: string;
|
|
837
|
+
source: "archcontext-default" | "environment";
|
|
838
|
+
};
|
|
839
|
+
warnings: string[];
|
|
840
|
+
}
|
package/src/product-version.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const ARCHCONTEXT_PRODUCT_NAME = "archctx";
|
|
2
|
-
export const ARCHCONTEXT_PRODUCT_VERSION = "0.5.
|
|
2
|
+
export const ARCHCONTEXT_PRODUCT_VERSION = "0.5.12";
|
|
3
3
|
export const ARCHCONTEXT_PACKAGE_MANAGER = "bun@1.4.0";
|
|
4
4
|
export const ARCHCONTEXT_NODE_RANGE = ">=22.22 <26";
|
|
5
5
|
export const LOCAL_RUNTIME_RPC_SCHEMA_VERSION = "archcontext.runtime-rpc/v1";
|
package/src/projection.ts
CHANGED
|
@@ -8,6 +8,7 @@ export const PROJECTION_APPLY_RECOVERY_BINDING_SCHEMA_VERSION = "archcontext.pro
|
|
|
8
8
|
export const PROJECTION_APPLY_RECOVERY_INTENT_SCHEMA_VERSION = "archcontext.projection-apply-recovery-intent/v1" as const;
|
|
9
9
|
export const PROJECTION_APPLY_RECOVERY_PROOF_SCHEMA_VERSION = "archcontext.projection-apply-recovery-proof/v1" as const;
|
|
10
10
|
export const PROJECTION_APPLY_RECOVERY_RESULT_SCHEMA_VERSION = "archcontext.projection-apply-recovery-result/v1" as const;
|
|
11
|
+
export const PROJECTION_APPLY_READBACK_RESULT_SCHEMA_VERSION = "archcontext.projection-apply-readback-result/v1" as const;
|
|
11
12
|
export const ARCHITECTURE_REFRESH_SIGNAL_SCHEMA_VERSION = "archcontext.architecture-refresh-signal/v1" as const;
|
|
12
13
|
export const ARCHCTX_CAPABILITIES_SCHEMA_VERSION = "archcontext.capabilities/v1" as const;
|
|
13
14
|
export const ARCHITECTURE_DOCS_RENDERER_VERSION = "archcontext.docs-renderer/v4" as const;
|
|
@@ -60,6 +61,7 @@ export const ARCHCTX_FEATURES = [
|
|
|
60
61
|
"architecture-docs-renderer-v2",
|
|
61
62
|
"architecture-refresh-signal-v1",
|
|
62
63
|
"module-statistics-v1",
|
|
64
|
+
"projection-apply-readback-v1",
|
|
63
65
|
"projection-apply-receipt-v1",
|
|
64
66
|
"projection-apply-recovery-v1",
|
|
65
67
|
"projection-prior-committed-applies-v1",
|
|
@@ -291,6 +293,28 @@ export interface ProjectionApplyRecoveryResultV1 {
|
|
|
291
293
|
refreshSignals: ArchitectureRefreshSignalV1[];
|
|
292
294
|
}
|
|
293
295
|
|
|
296
|
+
/** Non-consuming receipt retrieval; current state is rebuilt for every response. */
|
|
297
|
+
export interface ProjectionApplyReadbackResultV1 {
|
|
298
|
+
schemaVersion: typeof PROJECTION_APPLY_READBACK_RESULT_SCHEMA_VERSION;
|
|
299
|
+
requestId: string;
|
|
300
|
+
requestDigest: Sha256Digest;
|
|
301
|
+
receipt: ProjectionApplyReceiptV1;
|
|
302
|
+
current: ProjectionApplyRecoveryProofV1["current"];
|
|
303
|
+
readbackDigest: Sha256Digest;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Exact-request absence observed under the same writer boundary used by apply. */
|
|
307
|
+
export interface ProjectionApplyAbsenceV1 {
|
|
308
|
+
schemaVersion: "archcontext.projection-apply-absence/v1";
|
|
309
|
+
requestId: string;
|
|
310
|
+
requestDigest: Sha256Digest;
|
|
311
|
+
lookupKey: Sha256Digest;
|
|
312
|
+
current: ProjectionExpectedSnapshotV1;
|
|
313
|
+
absenceDigest: Sha256Digest;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export type ProjectionApplyReadbackV1 = ProjectionApplyReadbackResultV1 | ProjectionApplyAbsenceV1;
|
|
317
|
+
|
|
294
318
|
export interface ArchctxCapabilitiesV1 {
|
|
295
319
|
schemaVersion: typeof ARCHCTX_CAPABILITIES_SCHEMA_VERSION;
|
|
296
320
|
package: {
|
|
@@ -550,6 +574,89 @@ export function projectionApplyRecoveryBindingInvariantIssues(
|
|
|
550
574
|
return issues;
|
|
551
575
|
}
|
|
552
576
|
|
|
577
|
+
/** Readback carries the original accepted apply request, never caller-authored receipt data. */
|
|
578
|
+
export function projectionApplyReadbackRequestInvariantIssues(request: ProjectionRequestV1): string[] {
|
|
579
|
+
const issues = projectionRequestInvariantIssues(request);
|
|
580
|
+
const allowed = new Set(["schemaVersion", "requestId", "profile", "mode", "targets", "changedPaths", "expected", "acceptedChange"]);
|
|
581
|
+
if (Object.keys(request).some((key) => !allowed.has(key))) issues.push("readback request contains unsupported fields");
|
|
582
|
+
if (request.mode !== "apply" || !request.acceptedChange) issues.push("readback requires an accepted apply request");
|
|
583
|
+
if (request.schemaVersion !== PROJECTION_REQUEST_SCHEMA_VERSION || request.profile !== "repo-harness/v1") issues.push("readback request protocol is invalid");
|
|
584
|
+
if (typeof request.requestId !== "string"
|
|
585
|
+
|| request.targets.some((target) => !(PROJECTION_TARGETS as readonly string[]).includes(target))
|
|
586
|
+
|| request.changedPaths.some((path) => typeof path !== "string" || !isRepoRelativePosixPath(path))) issues.push("readback request fields are invalid");
|
|
587
|
+
const expectedKeys = ["repositoryId", "workspaceId", "headSha", "worktreeDigest"];
|
|
588
|
+
if (Object.keys(request.expected).some((key) => !expectedKeys.includes(key))
|
|
589
|
+
|| expectedKeys.some((key) => typeof request.expected[key as keyof ProjectionExpectedSnapshotV1] !== "string")
|
|
590
|
+
|| !request.expected.repositoryId || !request.expected.workspaceId
|
|
591
|
+
|| !/^[a-f0-9]{40}$/.test(request.expected.headSha)
|
|
592
|
+
|| !SHA256_DIGEST.test(request.expected.worktreeDigest)) issues.push("readback expected snapshot is invalid");
|
|
593
|
+
if (request.acceptedChange && Object.keys(request.acceptedChange).some((key) => !["changeSetId", "eventId", "reasonCodes", "affectedNodeIds"].includes(key))) {
|
|
594
|
+
issues.push("readback accepted change contains unsupported fields");
|
|
595
|
+
}
|
|
596
|
+
return issues;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export function projectionApplyAbsenceInvariantIssues(input: ProjectionApplyAbsenceV1, request: ProjectionRequestV1): string[] {
|
|
600
|
+
const issues = projectionApplyReadbackRequestInvariantIssues(request);
|
|
601
|
+
if (input.schemaVersion !== "archcontext.projection-apply-absence/v1") issues.push("absence schemaVersion is invalid");
|
|
602
|
+
if (Object.keys(input).some((key) => !["schemaVersion", "requestId", "requestDigest", "lookupKey", "current", "absenceDigest"].includes(key))) issues.push("absence contains unsupported fields");
|
|
603
|
+
const { absenceDigest, ...body } = input;
|
|
604
|
+
if (absenceDigest !== digestJson(body as unknown as Json)) issues.push("absenceDigest must bind the complete absence result");
|
|
605
|
+
if (input.requestId !== request.requestId || input.requestDigest !== digestJson(request as unknown as Json)) issues.push("absence request identity mismatch");
|
|
606
|
+
if (digestJson(input.current as unknown as Json) !== digestJson(request.expected as unknown as Json)) issues.push("absence current snapshot differs from request");
|
|
607
|
+
if (!request.acceptedChange || input.lookupKey !== projectionApplyLookupKey({
|
|
608
|
+
repositoryId: request.expected.repositoryId, workspaceId: request.expected.workspaceId, acceptedChange: request.acceptedChange
|
|
609
|
+
})) issues.push("absence lookup identity mismatch");
|
|
610
|
+
return issues;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export function projectionApplyReadbackResultDigest(input: Omit<ProjectionApplyReadbackResultV1, "readbackDigest">): Sha256Digest {
|
|
614
|
+
return digestJson(input as unknown as Json) as Sha256Digest;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export function projectionApplyReadbackResultInvariantIssues(
|
|
618
|
+
input: ProjectionApplyReadbackResultV1,
|
|
619
|
+
request?: ProjectionRequestV1
|
|
620
|
+
): string[] {
|
|
621
|
+
const issues = projectionApplyReceiptInvariantIssues(input.receipt);
|
|
622
|
+
const { readbackDigest, ...body } = input;
|
|
623
|
+
if (input.schemaVersion !== PROJECTION_APPLY_READBACK_RESULT_SCHEMA_VERSION) issues.push("readback schemaVersion is invalid");
|
|
624
|
+
if (projectionApplyReadbackResultDigest(body) !== readbackDigest) issues.push("readbackDigest must bind the complete readback result");
|
|
625
|
+
if (!/^[a-zA-Z0-9_.:-]+$/.test(input.requestId)) issues.push("readback requestId is invalid");
|
|
626
|
+
for (const value of [input.requestDigest, input.current.ownedOutputDigest, input.current.fixedPointDigest]) {
|
|
627
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(value)) issues.push("readback digest is invalid");
|
|
628
|
+
}
|
|
629
|
+
const receipt = input.receipt;
|
|
630
|
+
if (receipt.schemaVersion !== PROJECTION_APPLY_RECEIPT_SCHEMA_VERSION) issues.push("readback receipt schemaVersion is invalid");
|
|
631
|
+
const binding = receipt.recovery;
|
|
632
|
+
if (!binding) return [...issues, "committed receipt lacks its original recovery binding"];
|
|
633
|
+
for (const field of ["repositoryId", "workspaceId", "headSha", "worktreeDigest"] as const) {
|
|
634
|
+
if (input.current.snapshot[field] !== binding.originalExpectedSnapshot[field]) issues.push(`readback current ${field} differs from approval`);
|
|
635
|
+
}
|
|
636
|
+
if (input.current.snapshot.generatedFrom.codeGraphStatus !== "ready"
|
|
637
|
+
|| input.current.snapshot.rendererVersion !== binding.rendererVersion
|
|
638
|
+
|| input.current.snapshot.layoutVersion !== binding.layoutVersion
|
|
639
|
+
|| input.current.snapshot.projectionInputDigest !== receipt.result.outputSnapshot.projectionInputDigest
|
|
640
|
+
|| input.current.snapshot.codeGraphDigest !== receipt.result.outputSnapshot.codeGraphDigest
|
|
641
|
+
|| digestJson(input.current.snapshot.generatedFrom as unknown as Json) !== digestJson(binding.generatedFrom as unknown as Json)
|
|
642
|
+
|| digestJson(input.current.resultingDigests as unknown as Json) !== digestJson(binding.expectedResultingDigests as unknown as Json)
|
|
643
|
+
|| input.current.ownedOutputDigest !== binding.ownedOutputDigest) {
|
|
644
|
+
issues.push("readback current state differs from committed recovery binding");
|
|
645
|
+
}
|
|
646
|
+
if (input.requestId !== receipt.result.requestId) issues.push("readback requestId differs from committed request");
|
|
647
|
+
if (request) {
|
|
648
|
+
issues.push(...projectionApplyReadbackRequestInvariantIssues(request));
|
|
649
|
+
if (input.requestDigest !== digestJson(request as unknown as Json) || input.requestId !== request.requestId) issues.push("readback request identity mismatch");
|
|
650
|
+
if (digestJson(request.expected as unknown as Json) !== digestJson(binding.originalExpectedSnapshot as unknown as Json)
|
|
651
|
+
|| digestJson(request.acceptedChange as unknown as Json) !== digestJson(receipt.identity.acceptedChange as unknown as Json)
|
|
652
|
+
|| digestJson(request.targets as unknown as Json) !== digestJson(binding.targets as unknown as Json)
|
|
653
|
+
|| digestJson(request.changedPaths as unknown as Json) !== digestJson(binding.changedPaths as unknown as Json)) {
|
|
654
|
+
issues.push("readback request differs from committed approval binding");
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return issues;
|
|
658
|
+
}
|
|
659
|
+
|
|
553
660
|
export function projectionApplyRecoveryIntentInvariantIssues(input: ProjectionApplyRecoveryIntentV1): string[] {
|
|
554
661
|
const issues: string[] = [];
|
|
555
662
|
if (input.schemaVersion !== PROJECTION_APPLY_RECOVERY_INTENT_SCHEMA_VERSION) issues.push("recovery intent schemaVersion is invalid");
|