@open-agent-toolkit/cli 0.2.65 → 0.2.66
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/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/configuration.md +19 -1
- package/assets/docs/cli-utilities/index.md +3 -0
- package/assets/docs/cli-utilities/remote-project-management.md +191 -0
- package/assets/docs/reference/cli-reference.md +12 -1
- package/assets/docs/reference/file-locations.md +22 -0
- package/assets/docs/reference/oat-directory-structure.md +30 -17
- package/assets/docs/reference/troubleshooting.md +44 -0
- package/assets/docs/workflows/skills/index.md +33 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-doctor/SKILL.md +4 -4
- package/assets/skills/oat-pjm-remote/SKILL.md +101 -0
- package/assets/skills/oat-pjm-remote/references/external-action-protocol.md +43 -0
- package/dist/commands/backlog/new.d.ts +1 -0
- package/dist/commands/backlog/new.d.ts.map +1 -1
- package/dist/commands/backlog/new.js +3 -1
- package/dist/commands/config/index.d.ts +1 -0
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +232 -1
- package/dist/commands/pjm/doctor.d.ts +2 -0
- package/dist/commands/pjm/doctor.d.ts.map +1 -1
- package/dist/commands/pjm/doctor.js +71 -1
- package/dist/commands/pjm/index.d.ts +2 -0
- package/dist/commands/pjm/index.d.ts.map +1 -1
- package/dist/commands/pjm/index.js +3 -0
- package/dist/commands/pjm/remote/__integration__/lifecycle-harness.d.ts +60 -0
- package/dist/commands/pjm/remote/__integration__/lifecycle-harness.d.ts.map +1 -0
- package/dist/commands/pjm/remote/__integration__/lifecycle-harness.js +151 -0
- package/dist/commands/pjm/remote/association.d.ts +27 -0
- package/dist/commands/pjm/remote/association.d.ts.map +1 -0
- package/dist/commands/pjm/remote/association.js +68 -0
- package/dist/commands/pjm/remote/authority.d.ts +210 -0
- package/dist/commands/pjm/remote/authority.d.ts.map +1 -0
- package/dist/commands/pjm/remote/authority.js +341 -0
- package/dist/commands/pjm/remote/batch.d.ts +15 -0
- package/dist/commands/pjm/remote/batch.d.ts.map +1 -0
- package/dist/commands/pjm/remote/batch.js +126 -0
- package/dist/commands/pjm/remote/closeout.d.ts +69 -0
- package/dist/commands/pjm/remote/closeout.d.ts.map +1 -0
- package/dist/commands/pjm/remote/closeout.js +208 -0
- package/dist/commands/pjm/remote/create-binding.d.ts +62 -0
- package/dist/commands/pjm/remote/create-binding.d.ts.map +1 -0
- package/dist/commands/pjm/remote/create-binding.js +126 -0
- package/dist/commands/pjm/remote/credential-safety.d.ts +7 -0
- package/dist/commands/pjm/remote/credential-safety.d.ts.map +1 -0
- package/dist/commands/pjm/remote/credential-safety.js +21 -0
- package/dist/commands/pjm/remote/discussion.d.ts +49 -0
- package/dist/commands/pjm/remote/discussion.d.ts.map +1 -0
- package/dist/commands/pjm/remote/discussion.js +124 -0
- package/dist/commands/pjm/remote/doctor.d.ts +22 -0
- package/dist/commands/pjm/remote/doctor.d.ts.map +1 -0
- package/dist/commands/pjm/remote/doctor.js +380 -0
- package/dist/commands/pjm/remote/external-action.d.ts +208 -0
- package/dist/commands/pjm/remote/external-action.d.ts.map +1 -0
- package/dist/commands/pjm/remote/external-action.js +367 -0
- package/dist/commands/pjm/remote/host-execution.d.ts +28 -0
- package/dist/commands/pjm/remote/host-execution.d.ts.map +1 -0
- package/dist/commands/pjm/remote/host-execution.js +63 -0
- package/dist/commands/pjm/remote/index.d.ts +41 -0
- package/dist/commands/pjm/remote/index.d.ts.map +1 -0
- package/dist/commands/pjm/remote/index.js +280 -0
- package/dist/commands/pjm/remote/jira-adf.d.ts +35 -0
- package/dist/commands/pjm/remote/jira-adf.d.ts.map +1 -0
- package/dist/commands/pjm/remote/jira-adf.js +169 -0
- package/dist/commands/pjm/remote/lifecycle.d.ts +163 -0
- package/dist/commands/pjm/remote/lifecycle.d.ts.map +1 -0
- package/dist/commands/pjm/remote/lifecycle.js +224 -0
- package/dist/commands/pjm/remote/local-projection.d.ts +30 -0
- package/dist/commands/pjm/remote/local-projection.d.ts.map +1 -0
- package/dist/commands/pjm/remote/local-projection.js +95 -0
- package/dist/commands/pjm/remote/managed-markdown.d.ts +30 -0
- package/dist/commands/pjm/remote/managed-markdown.d.ts.map +1 -0
- package/dist/commands/pjm/remote/managed-markdown.js +128 -0
- package/dist/commands/pjm/remote/migrate.d.ts +14 -0
- package/dist/commands/pjm/remote/migrate.d.ts.map +1 -0
- package/dist/commands/pjm/remote/migrate.js +32 -0
- package/dist/commands/pjm/remote/operation-state.d.ts +15 -0
- package/dist/commands/pjm/remote/operation-state.d.ts.map +1 -0
- package/dist/commands/pjm/remote/operation-state.js +143 -0
- package/dist/commands/pjm/remote/outbound-projection-safety.d.ts +25 -0
- package/dist/commands/pjm/remote/outbound-projection-safety.d.ts.map +1 -0
- package/dist/commands/pjm/remote/outbound-projection-safety.js +99 -0
- package/dist/commands/pjm/remote/output.d.ts +60 -0
- package/dist/commands/pjm/remote/output.d.ts.map +1 -0
- package/dist/commands/pjm/remote/output.js +48 -0
- package/dist/commands/pjm/remote/preview.d.ts +88 -0
- package/dist/commands/pjm/remote/preview.d.ts.map +1 -0
- package/dist/commands/pjm/remote/preview.js +154 -0
- package/dist/commands/pjm/remote/provider-conformance.d.ts +13 -0
- package/dist/commands/pjm/remote/provider-conformance.d.ts.map +1 -0
- package/dist/commands/pjm/remote/provider-conformance.js +37 -0
- package/dist/commands/pjm/remote/provider.d.ts +57 -0
- package/dist/commands/pjm/remote/provider.d.ts.map +1 -0
- package/dist/commands/pjm/remote/provider.js +28 -0
- package/dist/commands/pjm/remote/providers/github-publication-safety.d.ts +46 -0
- package/dist/commands/pjm/remote/providers/github-publication-safety.d.ts.map +1 -0
- package/dist/commands/pjm/remote/providers/github-publication-safety.js +126 -0
- package/dist/commands/pjm/remote/providers/github.d.ts +207 -0
- package/dist/commands/pjm/remote/providers/github.d.ts.map +1 -0
- package/dist/commands/pjm/remote/providers/github.js +1456 -0
- package/dist/commands/pjm/remote/providers/jira.d.ts +245 -0
- package/dist/commands/pjm/remote/providers/jira.d.ts.map +1 -0
- package/dist/commands/pjm/remote/providers/jira.js +1650 -0
- package/dist/commands/pjm/remote/providers/linear.d.ts +182 -0
- package/dist/commands/pjm/remote/providers/linear.d.ts.map +1 -0
- package/dist/commands/pjm/remote/providers/linear.js +1289 -0
- package/dist/commands/pjm/remote/purpose-policy.d.ts +29 -0
- package/dist/commands/pjm/remote/purpose-policy.d.ts.map +1 -0
- package/dist/commands/pjm/remote/purpose-policy.js +118 -0
- package/dist/commands/pjm/remote/reconcile.d.ts +42 -0
- package/dist/commands/pjm/remote/reconcile.d.ts.map +1 -0
- package/dist/commands/pjm/remote/reconcile.js +101 -0
- package/dist/commands/pjm/remote/resolution.d.ts +122 -0
- package/dist/commands/pjm/remote/resolution.d.ts.map +1 -0
- package/dist/commands/pjm/remote/resolution.js +243 -0
- package/dist/commands/pjm/remote/schema.d.ts +9309 -0
- package/dist/commands/pjm/remote/schema.d.ts.map +1 -0
- package/dist/commands/pjm/remote/schema.js +1503 -0
- package/dist/commands/pjm/remote/service.d.ts +20 -0
- package/dist/commands/pjm/remote/service.d.ts.map +1 -0
- package/dist/commands/pjm/remote/service.js +5121 -0
- package/dist/commands/pjm/remote/shared-storage.d.ts +36 -0
- package/dist/commands/pjm/remote/shared-storage.d.ts.map +1 -0
- package/dist/commands/pjm/remote/shared-storage.js +110 -0
- package/dist/commands/pjm/remote/snapshot.d.ts +32 -0
- package/dist/commands/pjm/remote/snapshot.d.ts.map +1 -0
- package/dist/commands/pjm/remote/snapshot.js +140 -0
- package/dist/commands/pjm/remote/storage-locator.d.ts +29 -0
- package/dist/commands/pjm/remote/storage-locator.d.ts.map +1 -0
- package/dist/commands/pjm/remote/storage-locator.js +62 -0
- package/dist/commands/pjm/remote/store.d.ts +85 -0
- package/dist/commands/pjm/remote/store.d.ts.map +1 -0
- package/dist/commands/pjm/remote/store.js +553 -0
- package/dist/commands/pjm/remote/verification.d.ts +40 -0
- package/dist/commands/pjm/remote/verification.d.ts.map +1 -0
- package/dist/commands/pjm/remote/verification.js +100 -0
- package/dist/commands/tools/shared/pack-manifest.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-manifest.js +1 -0
- package/dist/config/oat-config.d.ts +26 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +186 -2
- package/package.json +2 -2
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { OatPjmRemoteOperationClass } from '../../../config/oat-config.js';
|
|
2
|
+
import type { RemoteBindingMetadata } from './schema.js';
|
|
3
|
+
export type BindingPurpose = RemoteBindingMetadata['purposes'][number];
|
|
4
|
+
export type SharedRemoteField = 'title' | 'description' | 'priority';
|
|
5
|
+
export type FieldDirection = 'inbound' | 'outbound';
|
|
6
|
+
export type CloseoutAnnotationPolicy = 'propose' | 'none';
|
|
7
|
+
export type CloseoutTransitionPolicy = 'propose' | 'provider-automation' | 'none';
|
|
8
|
+
export interface PurposePolicy {
|
|
9
|
+
fields: Record<SharedRemoteField, readonly FieldDirection[]>;
|
|
10
|
+
lifecycle: readonly OatPjmRemoteOperationClass[];
|
|
11
|
+
closeout: {
|
|
12
|
+
annotation: CloseoutAnnotationPolicy;
|
|
13
|
+
transition: CloseoutTransitionPolicy;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface ComposedPurposePolicy {
|
|
17
|
+
purposes: BindingPurpose[];
|
|
18
|
+
fields: Record<SharedRemoteField, FieldDirection[]>;
|
|
19
|
+
lifecycle: OatPjmRemoteOperationClass[];
|
|
20
|
+
closeout: {
|
|
21
|
+
annotation: CloseoutAnnotationPolicy;
|
|
22
|
+
transition: CloseoutTransitionPolicy | 'choice-required';
|
|
23
|
+
};
|
|
24
|
+
choiceRequired: boolean;
|
|
25
|
+
noOp: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare const PURPOSE_POLICIES: Readonly<Record<BindingPurpose, PurposePolicy>>;
|
|
28
|
+
export declare function composePurposePolicies(purposes: readonly BindingPurpose[]): ComposedPurposePolicy;
|
|
29
|
+
//# sourceMappingURL=purpose-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"purpose-policy.d.ts","sourceRoot":"","sources":["../../../../src/commands/pjm/remote/purpose-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAErE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEtD,MAAM,MAAM,cAAc,GAAG,qBAAqB,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AACvE,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,aAAa,GAAG,UAAU,CAAC;AACrE,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,UAAU,CAAC;AACpD,MAAM,MAAM,wBAAwB,GAAG,SAAS,GAAG,MAAM,CAAC;AAC1D,MAAM,MAAM,wBAAwB,GAChC,SAAS,GACT,qBAAqB,GACrB,MAAM,CAAC;AAEX,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC,iBAAiB,EAAE,SAAS,cAAc,EAAE,CAAC,CAAC;IAC7D,SAAS,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACjD,QAAQ,EAAE;QACR,UAAU,EAAE,wBAAwB,CAAC;QACrC,UAAU,EAAE,wBAAwB,CAAC;KACtC,CAAC;CACH;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC,iBAAiB,EAAE,cAAc,EAAE,CAAC,CAAC;IACpD,SAAS,EAAE,0BAA0B,EAAE,CAAC;IACxC,QAAQ,EAAE;QACR,UAAU,EAAE,wBAAwB,CAAC;QACrC,UAAU,EAAE,wBAAwB,GAAG,iBAAiB,CAAC;KAC1D,CAAC;IACF,cAAc,EAAE,OAAO,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;CACf;AAWD,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,cAAc,EAAE,aAAa,CAAC,CAkDzE,CAAC;AAkBL,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,SAAS,cAAc,EAAE,GAClC,qBAAqB,CA2DvB"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const INBOUND = Object.freeze(['inbound']);
|
|
2
|
+
const BIDIRECTIONAL = Object.freeze(['inbound', 'outbound']);
|
|
3
|
+
const NO_DIRECTIONS = Object.freeze([]);
|
|
4
|
+
const CLOSEOUT_LIFECYCLE = Object.freeze([
|
|
5
|
+
'annotate',
|
|
6
|
+
'transition',
|
|
7
|
+
]);
|
|
8
|
+
const NO_LIFECYCLE = Object.freeze([]);
|
|
9
|
+
export const PURPOSE_POLICIES = Object.freeze({
|
|
10
|
+
source: Object.freeze({
|
|
11
|
+
fields: Object.freeze({
|
|
12
|
+
title: INBOUND,
|
|
13
|
+
description: INBOUND,
|
|
14
|
+
priority: INBOUND,
|
|
15
|
+
}),
|
|
16
|
+
lifecycle: CLOSEOUT_LIFECYCLE,
|
|
17
|
+
closeout: Object.freeze({
|
|
18
|
+
annotation: 'propose',
|
|
19
|
+
transition: 'propose',
|
|
20
|
+
}),
|
|
21
|
+
}),
|
|
22
|
+
planning: Object.freeze({
|
|
23
|
+
fields: Object.freeze({
|
|
24
|
+
title: BIDIRECTIONAL,
|
|
25
|
+
description: BIDIRECTIONAL,
|
|
26
|
+
priority: BIDIRECTIONAL,
|
|
27
|
+
}),
|
|
28
|
+
lifecycle: CLOSEOUT_LIFECYCLE,
|
|
29
|
+
closeout: Object.freeze({
|
|
30
|
+
annotation: 'propose',
|
|
31
|
+
transition: 'propose',
|
|
32
|
+
}),
|
|
33
|
+
}),
|
|
34
|
+
delivery: Object.freeze({
|
|
35
|
+
fields: Object.freeze({
|
|
36
|
+
title: NO_DIRECTIONS,
|
|
37
|
+
description: NO_DIRECTIONS,
|
|
38
|
+
priority: NO_DIRECTIONS,
|
|
39
|
+
}),
|
|
40
|
+
lifecycle: NO_LIFECYCLE,
|
|
41
|
+
closeout: Object.freeze({
|
|
42
|
+
annotation: 'none',
|
|
43
|
+
transition: 'provider-automation',
|
|
44
|
+
}),
|
|
45
|
+
}),
|
|
46
|
+
reference: Object.freeze({
|
|
47
|
+
fields: Object.freeze({
|
|
48
|
+
title: NO_DIRECTIONS,
|
|
49
|
+
description: NO_DIRECTIONS,
|
|
50
|
+
priority: NO_DIRECTIONS,
|
|
51
|
+
}),
|
|
52
|
+
lifecycle: NO_LIFECYCLE,
|
|
53
|
+
closeout: Object.freeze({
|
|
54
|
+
annotation: 'none',
|
|
55
|
+
transition: 'none',
|
|
56
|
+
}),
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
const SHARED_FIELDS = [
|
|
60
|
+
'title',
|
|
61
|
+
'description',
|
|
62
|
+
'priority',
|
|
63
|
+
];
|
|
64
|
+
const OPERATION_ORDER = [
|
|
65
|
+
'create',
|
|
66
|
+
'update-fields',
|
|
67
|
+
'annotate',
|
|
68
|
+
'transition',
|
|
69
|
+
'delete',
|
|
70
|
+
'relink',
|
|
71
|
+
'detach',
|
|
72
|
+
'recreate',
|
|
73
|
+
];
|
|
74
|
+
export function composePurposePolicies(purposes) {
|
|
75
|
+
if (purposes.length === 0) {
|
|
76
|
+
throw new Error('A binding must declare at least one purpose.');
|
|
77
|
+
}
|
|
78
|
+
const uniquePurposes = new Set();
|
|
79
|
+
for (const purpose of purposes) {
|
|
80
|
+
if (!(purpose in PURPOSE_POLICIES)) {
|
|
81
|
+
throw new Error(`Unknown purpose '${String(purpose)}'.`);
|
|
82
|
+
}
|
|
83
|
+
if (uniquePurposes.has(purpose)) {
|
|
84
|
+
throw new Error('Binding purposes must be unique.');
|
|
85
|
+
}
|
|
86
|
+
uniquePurposes.add(purpose);
|
|
87
|
+
}
|
|
88
|
+
const policies = purposes.map((purpose) => PURPOSE_POLICIES[purpose]);
|
|
89
|
+
const fields = Object.fromEntries(SHARED_FIELDS.map((field) => [
|
|
90
|
+
field,
|
|
91
|
+
intersectOrdered(policies.map((policy) => policy.fields[field]), ['inbound', 'outbound']),
|
|
92
|
+
]));
|
|
93
|
+
const lifecycle = intersectOrdered(policies.map((policy) => policy.lifecycle), OPERATION_ORDER);
|
|
94
|
+
const annotation = policies.every((policy) => policy.closeout.annotation === 'propose')
|
|
95
|
+
? 'propose'
|
|
96
|
+
: 'none';
|
|
97
|
+
const transitionValues = new Set(policies.map((policy) => policy.closeout.transition));
|
|
98
|
+
const nonNoneTransitions = new Set([...transitionValues].filter((value) => value !== 'none'));
|
|
99
|
+
const transition = transitionValues.has('none')
|
|
100
|
+
? 'none'
|
|
101
|
+
: nonNoneTransitions.size > 1
|
|
102
|
+
? 'choice-required'
|
|
103
|
+
: ([...nonNoneTransitions][0] ?? 'none');
|
|
104
|
+
const choiceRequired = transition === 'choice-required';
|
|
105
|
+
const noOp = SHARED_FIELDS.every((field) => fields[field].length === 0) &&
|
|
106
|
+
lifecycle.length === 0;
|
|
107
|
+
return {
|
|
108
|
+
purposes: [...purposes],
|
|
109
|
+
fields,
|
|
110
|
+
lifecycle,
|
|
111
|
+
closeout: { annotation, transition },
|
|
112
|
+
choiceRequired,
|
|
113
|
+
noOp,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function intersectOrdered(values, order) {
|
|
117
|
+
return order.filter((candidate) => values.every((value) => value.includes(candidate)));
|
|
118
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { OatPjmRemoteDescriptionMode } from '../../../config/oat-config.js';
|
|
2
|
+
import type { FieldDirection, SharedRemoteField } from './purpose-policy.js';
|
|
3
|
+
export type ReconciliationClass = 'no-change' | 'local-only' | 'remote-only' | 'converged' | 'disjoint' | 'conflict' | 'remote-anomaly' | 'uncertain-operation';
|
|
4
|
+
export type ReconciliationDirection = 'inbound' | 'outbound' | 'none' | 'choice-required';
|
|
5
|
+
export type RemoteLifecycleCondition = 'active' | 'archived' | 'moved' | 'missing-or-invisible' | 'deleted-confirmed' | 'temporarily-unavailable';
|
|
6
|
+
export interface SharedFieldValues {
|
|
7
|
+
title: string | null;
|
|
8
|
+
description: string | null;
|
|
9
|
+
priority: string | null;
|
|
10
|
+
}
|
|
11
|
+
export interface ReconciliationInput {
|
|
12
|
+
base: SharedFieldValues;
|
|
13
|
+
local: SharedFieldValues;
|
|
14
|
+
remote: SharedFieldValues;
|
|
15
|
+
fieldDirections: Record<SharedRemoteField, readonly FieldDirection[]>;
|
|
16
|
+
descriptionMode: OatPjmRemoteDescriptionMode;
|
|
17
|
+
priorityMapping: boolean;
|
|
18
|
+
remoteLifecycle: RemoteLifecycleCondition;
|
|
19
|
+
uncertainOperation: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface FieldReconciliation {
|
|
22
|
+
field: SharedRemoteField;
|
|
23
|
+
base: string | null;
|
|
24
|
+
local: string | null;
|
|
25
|
+
remote: string | null;
|
|
26
|
+
classification: Exclude<ReconciliationClass, 'disjoint' | 'remote-anomaly' | 'uncertain-operation'>;
|
|
27
|
+
proposedDirection: ReconciliationDirection;
|
|
28
|
+
}
|
|
29
|
+
export interface BindingReconciliationPreview {
|
|
30
|
+
classification: ReconciliationClass;
|
|
31
|
+
fields: {
|
|
32
|
+
title: FieldReconciliation;
|
|
33
|
+
description: FieldReconciliation & {
|
|
34
|
+
scope: 'none' | 'managed-section' | 'full';
|
|
35
|
+
};
|
|
36
|
+
priority?: FieldReconciliation;
|
|
37
|
+
};
|
|
38
|
+
choiceRequired: boolean;
|
|
39
|
+
blockedBy: string[];
|
|
40
|
+
}
|
|
41
|
+
export declare function reconcileBinding(input: ReconciliationInput): BindingReconciliationPreview;
|
|
42
|
+
//# sourceMappingURL=reconcile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../../../src/commands/pjm/remote/reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,oBAAoB,CAAC;AAEtE,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1E,MAAM,MAAM,mBAAmB,GAC3B,WAAW,GACX,YAAY,GACZ,aAAa,GACb,WAAW,GACX,UAAU,GACV,UAAU,GACV,gBAAgB,GAChB,qBAAqB,CAAC;AAE1B,MAAM,MAAM,uBAAuB,GAC/B,SAAS,GACT,UAAU,GACV,MAAM,GACN,iBAAiB,CAAC;AAEtB,MAAM,MAAM,wBAAwB,GAChC,QAAQ,GACR,UAAU,GACV,OAAO,GACP,sBAAsB,GACtB,mBAAmB,GACnB,yBAAyB,CAAC;AAE9B,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,iBAAiB,CAAC;IACxB,KAAK,EAAE,iBAAiB,CAAC;IACzB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC,iBAAiB,EAAE,SAAS,cAAc,EAAE,CAAC,CAAC;IACtE,eAAe,EAAE,2BAA2B,CAAC;IAC7C,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,EAAE,wBAAwB,CAAC;IAC1C,kBAAkB,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,iBAAiB,CAAC;IACzB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,cAAc,EAAE,OAAO,CACrB,mBAAmB,EACnB,UAAU,GAAG,gBAAgB,GAAG,qBAAqB,CACtD,CAAC;IACF,iBAAiB,EAAE,uBAAuB,CAAC;CAC5C;AAED,MAAM,WAAW,4BAA4B;IAC3C,cAAc,EAAE,mBAAmB,CAAC;IACpC,MAAM,EAAE;QACN,KAAK,EAAE,mBAAmB,CAAC;QAC3B,WAAW,EAAE,mBAAmB,GAAG;YACjC,KAAK,EAAE,MAAM,GAAG,iBAAiB,GAAG,MAAM,CAAC;SAC5C,CAAC;QACF,QAAQ,CAAC,EAAE,mBAAmB,CAAC;KAChC,CAAC;IACF,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,mBAAmB,GACzB,4BAA4B,CA6C9B"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export function reconcileBinding(input) {
|
|
2
|
+
const blockedBy = [];
|
|
3
|
+
if (!input.priorityMapping) {
|
|
4
|
+
blockedBy.push('priority-mapping-unavailable');
|
|
5
|
+
}
|
|
6
|
+
const title = reconcileField('title', input);
|
|
7
|
+
const description = {
|
|
8
|
+
...reconcileField('description', input),
|
|
9
|
+
scope: descriptionScope(input.descriptionMode),
|
|
10
|
+
};
|
|
11
|
+
if (input.descriptionMode === 'none' &&
|
|
12
|
+
description.proposedDirection === 'outbound') {
|
|
13
|
+
description.proposedDirection = 'none';
|
|
14
|
+
}
|
|
15
|
+
const priority = input.priorityMapping
|
|
16
|
+
? reconcileField('priority', input)
|
|
17
|
+
: undefined;
|
|
18
|
+
const fieldList = [title, description, ...(priority ? [priority] : [])];
|
|
19
|
+
let classification = classifyBinding(fieldList);
|
|
20
|
+
if (input.uncertainOperation) {
|
|
21
|
+
classification = 'uncertain-operation';
|
|
22
|
+
blockedBy.unshift('uncertain-operation');
|
|
23
|
+
blockDirections(fieldList);
|
|
24
|
+
}
|
|
25
|
+
else if (input.remoteLifecycle !== 'active') {
|
|
26
|
+
classification = 'remote-anomaly';
|
|
27
|
+
blockedBy.unshift(`remote-lifecycle:${input.remoteLifecycle}`);
|
|
28
|
+
blockDirections(fieldList);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
classification,
|
|
32
|
+
fields: {
|
|
33
|
+
title,
|
|
34
|
+
description,
|
|
35
|
+
...(priority ? { priority } : {}),
|
|
36
|
+
},
|
|
37
|
+
choiceRequired: fieldList.some((field) => field.proposedDirection === 'choice-required'),
|
|
38
|
+
blockedBy,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function reconcileField(field, input) {
|
|
42
|
+
const base = input.base[field];
|
|
43
|
+
const local = input.local[field];
|
|
44
|
+
const remote = input.remote[field];
|
|
45
|
+
const classification = classifyField(base, local, remote);
|
|
46
|
+
return {
|
|
47
|
+
field,
|
|
48
|
+
base,
|
|
49
|
+
local,
|
|
50
|
+
remote,
|
|
51
|
+
classification,
|
|
52
|
+
proposedDirection: proposedDirection(classification, input.fieldDirections[field]),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function classifyField(base, local, remote) {
|
|
56
|
+
if (local === remote)
|
|
57
|
+
return local === base ? 'no-change' : 'converged';
|
|
58
|
+
const localChanged = local !== base;
|
|
59
|
+
const remoteChanged = remote !== base;
|
|
60
|
+
if (localChanged && remoteChanged)
|
|
61
|
+
return 'conflict';
|
|
62
|
+
if (localChanged)
|
|
63
|
+
return 'local-only';
|
|
64
|
+
if (remoteChanged)
|
|
65
|
+
return 'remote-only';
|
|
66
|
+
return 'no-change';
|
|
67
|
+
}
|
|
68
|
+
function proposedDirection(classification, allowed) {
|
|
69
|
+
if (classification === 'conflict')
|
|
70
|
+
return 'choice-required';
|
|
71
|
+
if (classification === 'local-only' && allowed.includes('outbound')) {
|
|
72
|
+
return 'outbound';
|
|
73
|
+
}
|
|
74
|
+
if (classification === 'remote-only' && allowed.includes('inbound')) {
|
|
75
|
+
return 'inbound';
|
|
76
|
+
}
|
|
77
|
+
return 'none';
|
|
78
|
+
}
|
|
79
|
+
function classifyBinding(fields) {
|
|
80
|
+
const classes = new Set(fields.map((field) => field.classification));
|
|
81
|
+
if (classes.has('conflict'))
|
|
82
|
+
return 'conflict';
|
|
83
|
+
if (classes.has('local-only') && classes.has('remote-only'))
|
|
84
|
+
return 'disjoint';
|
|
85
|
+
if (classes.has('local-only'))
|
|
86
|
+
return 'local-only';
|
|
87
|
+
if (classes.has('remote-only'))
|
|
88
|
+
return 'remote-only';
|
|
89
|
+
if (classes.has('converged'))
|
|
90
|
+
return 'converged';
|
|
91
|
+
return 'no-change';
|
|
92
|
+
}
|
|
93
|
+
function descriptionScope(mode) {
|
|
94
|
+
if (mode === 'replace')
|
|
95
|
+
return 'full';
|
|
96
|
+
return mode;
|
|
97
|
+
}
|
|
98
|
+
function blockDirections(fields) {
|
|
99
|
+
for (const field of fields)
|
|
100
|
+
field.proposedDirection = 'none';
|
|
101
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { type RemoteProvider } from './provider.js';
|
|
2
|
+
export interface ResolutionIdentity {
|
|
3
|
+
stableId: string;
|
|
4
|
+
context: Record<string, string | undefined>;
|
|
5
|
+
aliases: Array<{
|
|
6
|
+
kind: 'url' | 'display' | 'key';
|
|
7
|
+
value: string;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
export interface ResolutionBinding {
|
|
11
|
+
bindingId: string;
|
|
12
|
+
provider: RemoteProvider;
|
|
13
|
+
targetRef: string;
|
|
14
|
+
remoteIdentity: ResolutionIdentity;
|
|
15
|
+
identityHistory: Array<{
|
|
16
|
+
provider: RemoteProvider;
|
|
17
|
+
identity: ResolutionIdentity;
|
|
18
|
+
replacedAt: string;
|
|
19
|
+
replacedByOperationId: string;
|
|
20
|
+
}>;
|
|
21
|
+
lifecycle: 'active' | 'blocked' | 'tombstoned';
|
|
22
|
+
snapshotDigest: string | null;
|
|
23
|
+
}
|
|
24
|
+
export interface ResolutionApproval {
|
|
25
|
+
previewDigest: string;
|
|
26
|
+
approvedAt: string;
|
|
27
|
+
source: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ResolutionJournal {
|
|
30
|
+
schemaVersion: 1;
|
|
31
|
+
operationId: string;
|
|
32
|
+
bindingId: string;
|
|
33
|
+
kind: 'relink' | 'detach' | 'recreate';
|
|
34
|
+
previewDigest: string;
|
|
35
|
+
state: 'pending' | 'blocked' | 'partial' | 'uncertain' | 'complete';
|
|
36
|
+
bindingTransitionCompleted: boolean;
|
|
37
|
+
associationCompleted: boolean;
|
|
38
|
+
resultingBinding: ResolutionBinding | null;
|
|
39
|
+
searchCompleted?: boolean;
|
|
40
|
+
createIntentPersisted?: boolean;
|
|
41
|
+
createAttempted?: boolean;
|
|
42
|
+
createdAt: string;
|
|
43
|
+
updatedAt: string;
|
|
44
|
+
}
|
|
45
|
+
export interface ResolutionStore {
|
|
46
|
+
findByIdentity(provider: RemoteProvider, identity: ResolutionIdentity): Promise<string | null>;
|
|
47
|
+
writeBinding(binding: ResolutionBinding): Promise<void>;
|
|
48
|
+
writeAssociation(input: {
|
|
49
|
+
targetRef: string;
|
|
50
|
+
bindingId: string | null;
|
|
51
|
+
referenceRef: string | null;
|
|
52
|
+
}): Promise<void>;
|
|
53
|
+
readJournal(operationId: string): Promise<ResolutionJournal | null>;
|
|
54
|
+
writeJournal(journal: ResolutionJournal): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
interface CommonResolutionInput {
|
|
57
|
+
operationId: string;
|
|
58
|
+
binding: ResolutionBinding;
|
|
59
|
+
now: string;
|
|
60
|
+
previewDigest: string;
|
|
61
|
+
approval?: ResolutionApproval;
|
|
62
|
+
crash?: (point: 'after-binding' | 'after-association') => void;
|
|
63
|
+
}
|
|
64
|
+
export declare function relinkBinding(input: CommonResolutionInput & {
|
|
65
|
+
replacement: {
|
|
66
|
+
identity: ResolutionIdentity;
|
|
67
|
+
verifiedAt: string;
|
|
68
|
+
evidenceDigest: string;
|
|
69
|
+
};
|
|
70
|
+
}, store: ResolutionStore): Promise<{
|
|
71
|
+
binding: ResolutionBinding;
|
|
72
|
+
journal: ResolutionJournal;
|
|
73
|
+
}>;
|
|
74
|
+
export declare function detachBinding(input: CommonResolutionInput & {
|
|
75
|
+
keepReference: boolean;
|
|
76
|
+
}, store: ResolutionStore): Promise<{
|
|
77
|
+
binding: ResolutionBinding;
|
|
78
|
+
journal: ResolutionJournal;
|
|
79
|
+
}>;
|
|
80
|
+
export type DuplicateSearchOutcome = {
|
|
81
|
+
kind: 'found-existing';
|
|
82
|
+
replacement: {
|
|
83
|
+
identity: ResolutionIdentity;
|
|
84
|
+
verifiedAt: string;
|
|
85
|
+
evidenceDigest: string;
|
|
86
|
+
};
|
|
87
|
+
} | {
|
|
88
|
+
kind: 'search-unavailable';
|
|
89
|
+
} | {
|
|
90
|
+
kind: 'ambiguous';
|
|
91
|
+
candidates: string[];
|
|
92
|
+
} | {
|
|
93
|
+
kind: 'no-match';
|
|
94
|
+
};
|
|
95
|
+
export type RecreateAttemptOutcome = {
|
|
96
|
+
kind: 'committed';
|
|
97
|
+
replacement: {
|
|
98
|
+
identity: ResolutionIdentity;
|
|
99
|
+
verifiedAt: string;
|
|
100
|
+
evidenceDigest: string;
|
|
101
|
+
};
|
|
102
|
+
} | {
|
|
103
|
+
kind: 'uncertain';
|
|
104
|
+
} | {
|
|
105
|
+
kind: 'rejected';
|
|
106
|
+
};
|
|
107
|
+
export declare function recreateBinding(input: CommonResolutionInput & {
|
|
108
|
+
searchDuplicates(): Promise<DuplicateSearchOutcome>;
|
|
109
|
+
createReplacement(): Promise<RecreateAttemptOutcome>;
|
|
110
|
+
}, store: ResolutionStore): Promise<{
|
|
111
|
+
status: 'verified' | 'blocked' | 'uncertain';
|
|
112
|
+
binding: ResolutionBinding;
|
|
113
|
+
journal: ResolutionJournal;
|
|
114
|
+
}>;
|
|
115
|
+
export declare function buildResolutionPreviewDigest(input: {
|
|
116
|
+
kind: ResolutionJournal['kind'];
|
|
117
|
+
binding: ResolutionBinding;
|
|
118
|
+
replacement?: ResolutionIdentity;
|
|
119
|
+
keepReference?: boolean;
|
|
120
|
+
}): string;
|
|
121
|
+
export {};
|
|
122
|
+
//# sourceMappingURL=resolution.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolution.d.ts","sourceRoot":"","sources":["../../../../src/commands/pjm/remote/resolution.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { semanticDigest } from './provider.js';
|
|
2
|
+
export async function relinkBinding(input, store) {
|
|
3
|
+
assertFreshApproval(input);
|
|
4
|
+
assertVerifiedReplacement(input.replacement);
|
|
5
|
+
const duplicate = await store.findByIdentity(input.binding.provider, input.replacement.identity);
|
|
6
|
+
if (duplicate && duplicate !== input.binding.bindingId) {
|
|
7
|
+
throw new Error(`Replacement identity is already bound by '${duplicate}'.`);
|
|
8
|
+
}
|
|
9
|
+
let journal = await store.readJournal(input.operationId);
|
|
10
|
+
assertJournalMatches(journal, input, 'relink');
|
|
11
|
+
if (!journal) {
|
|
12
|
+
journal = createJournal(input, 'relink');
|
|
13
|
+
await store.writeJournal(journal);
|
|
14
|
+
}
|
|
15
|
+
if (!journal.bindingTransitionCompleted) {
|
|
16
|
+
const nextBinding = {
|
|
17
|
+
...input.binding,
|
|
18
|
+
remoteIdentity: structuredClone(input.replacement.identity),
|
|
19
|
+
identityHistory: [
|
|
20
|
+
...input.binding.identityHistory,
|
|
21
|
+
{
|
|
22
|
+
provider: input.binding.provider,
|
|
23
|
+
identity: structuredClone(input.binding.remoteIdentity),
|
|
24
|
+
replacedAt: input.now,
|
|
25
|
+
replacedByOperationId: input.operationId,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
lifecycle: 'active',
|
|
29
|
+
};
|
|
30
|
+
await store.writeBinding(nextBinding);
|
|
31
|
+
journal = {
|
|
32
|
+
...journal,
|
|
33
|
+
state: 'partial',
|
|
34
|
+
bindingTransitionCompleted: true,
|
|
35
|
+
resultingBinding: nextBinding,
|
|
36
|
+
updatedAt: input.now,
|
|
37
|
+
};
|
|
38
|
+
await store.writeJournal(journal);
|
|
39
|
+
input.crash?.('after-binding');
|
|
40
|
+
}
|
|
41
|
+
if (!journal.associationCompleted) {
|
|
42
|
+
await store.writeAssociation({
|
|
43
|
+
targetRef: input.binding.targetRef,
|
|
44
|
+
bindingId: input.binding.bindingId,
|
|
45
|
+
referenceRef: null,
|
|
46
|
+
});
|
|
47
|
+
journal = completeAssociation(journal, input.now);
|
|
48
|
+
await store.writeJournal(journal);
|
|
49
|
+
input.crash?.('after-association');
|
|
50
|
+
}
|
|
51
|
+
return { binding: journal.resultingBinding, journal };
|
|
52
|
+
}
|
|
53
|
+
export async function detachBinding(input, store) {
|
|
54
|
+
assertFreshApproval(input);
|
|
55
|
+
let journal = await store.readJournal(input.operationId);
|
|
56
|
+
assertJournalMatches(journal, input, 'detach');
|
|
57
|
+
if (!journal) {
|
|
58
|
+
journal = createJournal(input, 'detach');
|
|
59
|
+
await store.writeJournal(journal);
|
|
60
|
+
}
|
|
61
|
+
if (!journal.bindingTransitionCompleted) {
|
|
62
|
+
const nextBinding = {
|
|
63
|
+
...input.binding,
|
|
64
|
+
lifecycle: 'tombstoned',
|
|
65
|
+
};
|
|
66
|
+
await store.writeBinding(nextBinding);
|
|
67
|
+
journal = {
|
|
68
|
+
...journal,
|
|
69
|
+
state: 'partial',
|
|
70
|
+
bindingTransitionCompleted: true,
|
|
71
|
+
resultingBinding: nextBinding,
|
|
72
|
+
updatedAt: input.now,
|
|
73
|
+
};
|
|
74
|
+
await store.writeJournal(journal);
|
|
75
|
+
input.crash?.('after-binding');
|
|
76
|
+
}
|
|
77
|
+
if (!journal.associationCompleted) {
|
|
78
|
+
await store.writeAssociation({
|
|
79
|
+
targetRef: input.binding.targetRef,
|
|
80
|
+
bindingId: null,
|
|
81
|
+
referenceRef: input.keepReference
|
|
82
|
+
? `${input.binding.provider}:${input.binding.remoteIdentity.stableId}`
|
|
83
|
+
: null,
|
|
84
|
+
});
|
|
85
|
+
journal = completeAssociation(journal, input.now);
|
|
86
|
+
await store.writeJournal(journal);
|
|
87
|
+
input.crash?.('after-association');
|
|
88
|
+
}
|
|
89
|
+
return { binding: journal.resultingBinding, journal };
|
|
90
|
+
}
|
|
91
|
+
export async function recreateBinding(input, store) {
|
|
92
|
+
assertFreshApproval(input);
|
|
93
|
+
let journal = await store.readJournal(input.operationId);
|
|
94
|
+
assertJournalMatches(journal, input, 'recreate');
|
|
95
|
+
if (journal?.createAttempted && !journal.bindingTransitionCompleted) {
|
|
96
|
+
return {
|
|
97
|
+
status: 'uncertain',
|
|
98
|
+
binding: { ...input.binding, lifecycle: 'blocked' },
|
|
99
|
+
journal,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (!journal) {
|
|
103
|
+
journal = createJournal(input, 'recreate');
|
|
104
|
+
await store.writeJournal(journal);
|
|
105
|
+
}
|
|
106
|
+
const search = await input.searchDuplicates();
|
|
107
|
+
journal = {
|
|
108
|
+
...journal,
|
|
109
|
+
searchCompleted: true,
|
|
110
|
+
updatedAt: input.now,
|
|
111
|
+
};
|
|
112
|
+
await store.writeJournal(journal);
|
|
113
|
+
if (search.kind === 'search-unavailable' || search.kind === 'ambiguous') {
|
|
114
|
+
journal = { ...journal, state: 'blocked', updatedAt: input.now };
|
|
115
|
+
await store.writeJournal(journal);
|
|
116
|
+
return { status: 'blocked', binding: input.binding, journal };
|
|
117
|
+
}
|
|
118
|
+
let replacement = null;
|
|
119
|
+
if (search.kind === 'found-existing') {
|
|
120
|
+
replacement = search.replacement;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
journal = {
|
|
124
|
+
...journal,
|
|
125
|
+
createIntentPersisted: true,
|
|
126
|
+
updatedAt: input.now,
|
|
127
|
+
};
|
|
128
|
+
await store.writeJournal(journal);
|
|
129
|
+
journal = { ...journal, createAttempted: true, updatedAt: input.now };
|
|
130
|
+
await store.writeJournal(journal);
|
|
131
|
+
const create = await input.createReplacement();
|
|
132
|
+
if (create.kind !== 'committed') {
|
|
133
|
+
journal = {
|
|
134
|
+
...journal,
|
|
135
|
+
state: create.kind === 'uncertain' ? 'uncertain' : 'blocked',
|
|
136
|
+
updatedAt: input.now,
|
|
137
|
+
};
|
|
138
|
+
await store.writeJournal(journal);
|
|
139
|
+
return {
|
|
140
|
+
status: create.kind === 'uncertain' ? 'uncertain' : 'blocked',
|
|
141
|
+
binding: { ...input.binding, lifecycle: 'blocked' },
|
|
142
|
+
journal,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
replacement = create.replacement;
|
|
146
|
+
}
|
|
147
|
+
assertVerifiedReplacement(replacement);
|
|
148
|
+
const duplicate = await store.findByIdentity(input.binding.provider, replacement.identity);
|
|
149
|
+
if (duplicate && duplicate !== input.binding.bindingId) {
|
|
150
|
+
throw new Error(`Replacement identity is already bound by '${duplicate}'.`);
|
|
151
|
+
}
|
|
152
|
+
if (!journal.bindingTransitionCompleted) {
|
|
153
|
+
const nextBinding = {
|
|
154
|
+
...input.binding,
|
|
155
|
+
remoteIdentity: structuredClone(replacement.identity),
|
|
156
|
+
identityHistory: [
|
|
157
|
+
...input.binding.identityHistory,
|
|
158
|
+
{
|
|
159
|
+
provider: input.binding.provider,
|
|
160
|
+
identity: structuredClone(input.binding.remoteIdentity),
|
|
161
|
+
replacedAt: input.now,
|
|
162
|
+
replacedByOperationId: input.operationId,
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
lifecycle: 'active',
|
|
166
|
+
};
|
|
167
|
+
await store.writeBinding(nextBinding);
|
|
168
|
+
journal = {
|
|
169
|
+
...journal,
|
|
170
|
+
state: 'partial',
|
|
171
|
+
bindingTransitionCompleted: true,
|
|
172
|
+
resultingBinding: nextBinding,
|
|
173
|
+
updatedAt: input.now,
|
|
174
|
+
};
|
|
175
|
+
await store.writeJournal(journal);
|
|
176
|
+
input.crash?.('after-binding');
|
|
177
|
+
}
|
|
178
|
+
if (!journal.associationCompleted) {
|
|
179
|
+
await store.writeAssociation({
|
|
180
|
+
targetRef: input.binding.targetRef,
|
|
181
|
+
bindingId: input.binding.bindingId,
|
|
182
|
+
referenceRef: null,
|
|
183
|
+
});
|
|
184
|
+
journal = completeAssociation(journal, input.now);
|
|
185
|
+
await store.writeJournal(journal);
|
|
186
|
+
input.crash?.('after-association');
|
|
187
|
+
}
|
|
188
|
+
return { status: 'verified', binding: journal.resultingBinding, journal };
|
|
189
|
+
}
|
|
190
|
+
function assertJournalMatches(journal, input, kind) {
|
|
191
|
+
if (journal &&
|
|
192
|
+
(journal.kind !== kind ||
|
|
193
|
+
journal.bindingId !== input.binding.bindingId ||
|
|
194
|
+
journal.previewDigest !== input.previewDigest)) {
|
|
195
|
+
throw new Error('Persisted resolution journal does not match the requested kind, binding, and preview.');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function assertFreshApproval(input) {
|
|
199
|
+
const approval = input.approval;
|
|
200
|
+
const age = approval
|
|
201
|
+
? Date.parse(input.now) - Date.parse(approval.approvedAt)
|
|
202
|
+
: Number.POSITIVE_INFINITY;
|
|
203
|
+
if (!approval ||
|
|
204
|
+
approval.previewDigest !== input.previewDigest ||
|
|
205
|
+
!Number.isFinite(age) ||
|
|
206
|
+
age < 0 ||
|
|
207
|
+
age > 5 * 60 * 1_000) {
|
|
208
|
+
throw new Error('Resolution requires fresh approval of the exact preview.');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function assertVerifiedReplacement(replacement) {
|
|
212
|
+
if (replacement.identity.stableId.length === 0 ||
|
|
213
|
+
replacement.evidenceDigest.length === 0 ||
|
|
214
|
+
!Number.isFinite(Date.parse(replacement.verifiedAt))) {
|
|
215
|
+
throw new Error('Relink requires a verified stable replacement identity.');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function createJournal(input, kind) {
|
|
219
|
+
return {
|
|
220
|
+
schemaVersion: 1,
|
|
221
|
+
operationId: input.operationId,
|
|
222
|
+
bindingId: input.binding.bindingId,
|
|
223
|
+
kind,
|
|
224
|
+
previewDigest: input.previewDigest,
|
|
225
|
+
state: 'pending',
|
|
226
|
+
bindingTransitionCompleted: false,
|
|
227
|
+
associationCompleted: false,
|
|
228
|
+
resultingBinding: null,
|
|
229
|
+
createdAt: input.now,
|
|
230
|
+
updatedAt: input.now,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function completeAssociation(journal, updatedAt) {
|
|
234
|
+
return {
|
|
235
|
+
...journal,
|
|
236
|
+
state: 'complete',
|
|
237
|
+
associationCompleted: true,
|
|
238
|
+
updatedAt,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
export function buildResolutionPreviewDigest(input) {
|
|
242
|
+
return semanticDigest(input);
|
|
243
|
+
}
|