@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,1289 @@
|
|
|
1
|
+
import { containsSensitiveContentSignal } from '../../../pjm/remote/credential-safety.js';
|
|
2
|
+
import { requireCurrentOutboundSafety, } from '../../../pjm/remote/outbound-projection-safety.js';
|
|
3
|
+
import { contextsEqual, semanticDigest, } from '../../../pjm/remote/provider.js';
|
|
4
|
+
import { WHOLE_FIELD_SUPPRESSION_MARKER } from '../../../pjm/remote/schema.js';
|
|
5
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
6
|
+
const IDENTIFIER = /^[A-Z][A-Z0-9]*-[1-9][0-9]*$/;
|
|
7
|
+
const URL_REFERENCE = /^https:\/\/([^/]+)\/[^/]+\/issue\/([A-Z][A-Z0-9]*-[1-9][0-9]*)(?:\/[^/?#]+)?(?:[?#].*)?$/;
|
|
8
|
+
const LINEAR_EXTENSION_KEYS = ['estimate', 'cycleId', 'projectId'];
|
|
9
|
+
/** Absolute UTF-8 bounds for one non-persisted Linear discussion page. */
|
|
10
|
+
export const LINEAR_DISCUSSION_LIMITS = {
|
|
11
|
+
maxItems: 100,
|
|
12
|
+
maxIdBytes: 256,
|
|
13
|
+
maxBodyBytes: 16_384,
|
|
14
|
+
maxCursorBytes: 512,
|
|
15
|
+
maxPageBytes: 65_536,
|
|
16
|
+
};
|
|
17
|
+
export function parseLinearIssueReference(reference) {
|
|
18
|
+
const value = reference.trim();
|
|
19
|
+
const url = URL_REFERENCE.exec(value);
|
|
20
|
+
if (url) {
|
|
21
|
+
return {
|
|
22
|
+
host: url[1].toLowerCase(),
|
|
23
|
+
identifier: url[2],
|
|
24
|
+
alias: value,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (!IDENTIFIER.test(value))
|
|
28
|
+
return null;
|
|
29
|
+
return { identifier: value, alias: value };
|
|
30
|
+
}
|
|
31
|
+
export function normalizeLinearIssueObservation(observation) {
|
|
32
|
+
assertLinearObservation(observation);
|
|
33
|
+
const workspaceId = requiredContext(observation, 'workspaceId');
|
|
34
|
+
const teamId = requiredContext(observation, 'teamId');
|
|
35
|
+
const uuid = requiredString(observation.fields, 'uuid').toLowerCase();
|
|
36
|
+
if (!UUID.test(uuid))
|
|
37
|
+
throw new Error('Linear issue UUID is invalid.');
|
|
38
|
+
if (observation.identity.stableId.toLowerCase() !== uuid &&
|
|
39
|
+
observation.identity.stableId !== canonicalLinearStableId(workspaceId, uuid)) {
|
|
40
|
+
throw new Error('Linear observation stable identity does not match UUID.');
|
|
41
|
+
}
|
|
42
|
+
if (observation.fields.workspaceId !== workspaceId) {
|
|
43
|
+
throw new Error('Linear observation workspace context does not match.');
|
|
44
|
+
}
|
|
45
|
+
if (observation.fields.teamId !== teamId) {
|
|
46
|
+
throw new Error('Linear observation team context does not match.');
|
|
47
|
+
}
|
|
48
|
+
const identifier = requiredString(observation.fields, 'identifier');
|
|
49
|
+
if (!IDENTIFIER.test(identifier)) {
|
|
50
|
+
throw new Error('Linear issue identifier is invalid.');
|
|
51
|
+
}
|
|
52
|
+
const historicalIdentifiers = stringArray(observation.fields.historicalIdentifiers).filter((alias) => alias !== identifier);
|
|
53
|
+
const historicalTeamIds = stringArray(observation.fields.historicalTeamIds).filter((historicalTeamId) => historicalTeamId !== teamId);
|
|
54
|
+
const suppressedFields = [];
|
|
55
|
+
const title = suppressInboundText(requiredString(observation.fields, 'title'), 'title', suppressedFields);
|
|
56
|
+
const description = suppressNullableInboundText(observation.fields.description, 'description', suppressedFields);
|
|
57
|
+
const extensions = {
|
|
58
|
+
uuid,
|
|
59
|
+
workspaceId,
|
|
60
|
+
teamId,
|
|
61
|
+
historicalTeamIds,
|
|
62
|
+
currentIdentifier: identifier,
|
|
63
|
+
historicalIdentifiers,
|
|
64
|
+
archived: requiredBoolean(observation.fields, 'archived'),
|
|
65
|
+
capabilityEvidenceDigest: observation.capabilityEvidenceDigest,
|
|
66
|
+
};
|
|
67
|
+
for (const key of LINEAR_EXTENSION_KEYS) {
|
|
68
|
+
const value = observation.fields[key];
|
|
69
|
+
if (value !== undefined && isBoundedExtensionValue(value)) {
|
|
70
|
+
extensions[key] = value;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (isRecord(observation.fields.mutationEvidence)) {
|
|
74
|
+
extensions.mutationEvidence = observation.fields.mutationEvidence;
|
|
75
|
+
}
|
|
76
|
+
if (isRecord(observation.fields.createProvenance)) {
|
|
77
|
+
extensions.createProvenance = observation.fields.createProvenance;
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(observation.fields.annotations)) {
|
|
80
|
+
extensions.annotations = stringArray(observation.fields.annotations);
|
|
81
|
+
}
|
|
82
|
+
extensions.suppressedFields = suppressedFields;
|
|
83
|
+
return {
|
|
84
|
+
provider: 'linear',
|
|
85
|
+
context: observation.context,
|
|
86
|
+
stableId: canonicalLinearStableId(workspaceId, uuid),
|
|
87
|
+
aliases: uniqueStrings([
|
|
88
|
+
identifier,
|
|
89
|
+
...historicalIdentifiers,
|
|
90
|
+
...observation.identity.aliases,
|
|
91
|
+
]),
|
|
92
|
+
title,
|
|
93
|
+
description,
|
|
94
|
+
priority: nullableString(observation.fields.priority),
|
|
95
|
+
status: requiredString(observation.fields, 'state'),
|
|
96
|
+
revisionDigest: semanticDigest(observation.revision),
|
|
97
|
+
extensions,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function validateLinearHostCapability(operation, context, observed, requiredFields = []) {
|
|
101
|
+
const reasons = [];
|
|
102
|
+
if (observed.provider !== 'linear')
|
|
103
|
+
reasons.push('provider-mismatch');
|
|
104
|
+
if (observed.availability === 'unavailable')
|
|
105
|
+
reasons.push('access-unavailable');
|
|
106
|
+
if (observed.availability === 'authorization-required')
|
|
107
|
+
reasons.push('authorization-required');
|
|
108
|
+
if (observed.availability === 'rate-limited')
|
|
109
|
+
reasons.push('rate-limited');
|
|
110
|
+
if (!hasPinnedLinearContext(context) ||
|
|
111
|
+
!contextsEqual(context, observed.context)) {
|
|
112
|
+
reasons.push('context-mismatch');
|
|
113
|
+
}
|
|
114
|
+
if (context.accountId !== observed.accountId)
|
|
115
|
+
reasons.push('account-mismatch');
|
|
116
|
+
if (context.workspaceId !== observed.workspaceId)
|
|
117
|
+
reasons.push('workspace-mismatch');
|
|
118
|
+
if (context.teamId !== observed.teamId)
|
|
119
|
+
reasons.push('team-mismatch');
|
|
120
|
+
if (!observed.operations.includes(operation)) {
|
|
121
|
+
reasons.push(`capability-missing:${operation}`);
|
|
122
|
+
}
|
|
123
|
+
for (const field of requiredFields) {
|
|
124
|
+
if (!observed.observableFields.includes(field)) {
|
|
125
|
+
reasons.push(`semantic-field-missing:${field}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (!observed.evidenceDigest)
|
|
129
|
+
reasons.push('capability-evidence-missing');
|
|
130
|
+
return { valid: reasons.length === 0, reasons };
|
|
131
|
+
}
|
|
132
|
+
export function planLinearRead(input) {
|
|
133
|
+
if (!hasPinnedLinearContext(input.context) ||
|
|
134
|
+
!validLinearStableIdForContext(input.stableId, input.context) ||
|
|
135
|
+
!UUID.test(input.uuid) ||
|
|
136
|
+
input.stableId !==
|
|
137
|
+
canonicalLinearStableId(input.context.workspaceId, input.uuid) ||
|
|
138
|
+
(input.currentIdentifier !== null &&
|
|
139
|
+
!IDENTIFIER.test(input.currentIdentifier)) ||
|
|
140
|
+
!input.stepId ||
|
|
141
|
+
Buffer.byteLength(input.stepId, 'utf8') > 128) {
|
|
142
|
+
throw new Error('Linear read plan requires pinned identity and context.');
|
|
143
|
+
}
|
|
144
|
+
const capability = validateLinearHostCapability('read', input.context, input.hostCapability, ['stable-identity', 'title', 'state', 'revision', 'team-context']);
|
|
145
|
+
if (!capability.valid) {
|
|
146
|
+
throw new Error(`Linear read capability is unavailable: ${capability.reasons.join(',')}`);
|
|
147
|
+
}
|
|
148
|
+
const intent = {
|
|
149
|
+
stableId: input.stableId,
|
|
150
|
+
uuid: input.uuid.toLowerCase(),
|
|
151
|
+
currentIdentifier: input.currentIdentifier,
|
|
152
|
+
stepId: input.stepId,
|
|
153
|
+
capabilityEvidenceDigest: input.hostCapability.evidenceDigest,
|
|
154
|
+
resultContract: {
|
|
155
|
+
requireStableIdentity: true,
|
|
156
|
+
requireExactContext: true,
|
|
157
|
+
allowedFields: [
|
|
158
|
+
'uuid',
|
|
159
|
+
'identifier',
|
|
160
|
+
'historicalIdentifiers',
|
|
161
|
+
'workspaceId',
|
|
162
|
+
'teamId',
|
|
163
|
+
'historicalTeamIds',
|
|
164
|
+
'title',
|
|
165
|
+
'description',
|
|
166
|
+
'state',
|
|
167
|
+
'priority',
|
|
168
|
+
'archived',
|
|
169
|
+
...LINEAR_EXTENSION_KEYS,
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
provider: 'linear',
|
|
175
|
+
operation: 'read',
|
|
176
|
+
context: input.context,
|
|
177
|
+
intent: {
|
|
178
|
+
...intent,
|
|
179
|
+
actionDigest: semanticDigest({
|
|
180
|
+
provider: 'linear',
|
|
181
|
+
operation: 'read',
|
|
182
|
+
context: input.context,
|
|
183
|
+
intent,
|
|
184
|
+
}),
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
export function planLinearDiscussionRead(input) {
|
|
189
|
+
const capability = validateLinearHostCapability('read-discussion', input.context, input.hostCapability);
|
|
190
|
+
if (!capability.valid) {
|
|
191
|
+
throw new Error('Linear discussion read capability is unavailable.');
|
|
192
|
+
}
|
|
193
|
+
if (!validLinearStableIdForContext(input.stableId, input.context) ||
|
|
194
|
+
!Number.isInteger(input.limit) ||
|
|
195
|
+
input.limit < 1 ||
|
|
196
|
+
input.limit > LINEAR_DISCUSSION_LIMITS.maxItems ||
|
|
197
|
+
(input.cursor !== null &&
|
|
198
|
+
(typeof input.cursor !== 'string' ||
|
|
199
|
+
Buffer.byteLength(input.cursor, 'utf8') >
|
|
200
|
+
LINEAR_DISCUSSION_LIMITS.maxCursorBytes))) {
|
|
201
|
+
throw new Error('Linear discussion read bounds are invalid.');
|
|
202
|
+
}
|
|
203
|
+
const intent = {
|
|
204
|
+
stableId: input.stableId,
|
|
205
|
+
cursor: input.cursor,
|
|
206
|
+
limit: input.limit,
|
|
207
|
+
resultContract: {
|
|
208
|
+
maxItems: input.limit,
|
|
209
|
+
maxIdBytes: LINEAR_DISCUSSION_LIMITS.maxIdBytes,
|
|
210
|
+
maxBodyBytes: LINEAR_DISCUSSION_LIMITS.maxBodyBytes,
|
|
211
|
+
maxCursorBytes: LINEAR_DISCUSSION_LIMITS.maxCursorBytes,
|
|
212
|
+
maxPageBytes: LINEAR_DISCUSSION_LIMITS.maxPageBytes,
|
|
213
|
+
persistable: false,
|
|
214
|
+
contentPolicy: 'sanitized-whole-field-suppression',
|
|
215
|
+
},
|
|
216
|
+
capabilityEvidenceDigest: input.hostCapability.evidenceDigest,
|
|
217
|
+
};
|
|
218
|
+
return {
|
|
219
|
+
provider: 'linear',
|
|
220
|
+
operation: 'read-discussion',
|
|
221
|
+
context: input.context,
|
|
222
|
+
intent: {
|
|
223
|
+
...intent,
|
|
224
|
+
actionDigest: semanticDigest({
|
|
225
|
+
provider: 'linear',
|
|
226
|
+
operation: 'read-discussion',
|
|
227
|
+
context: input.context,
|
|
228
|
+
intent,
|
|
229
|
+
}),
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
export function previewLinearMutation(input) {
|
|
234
|
+
const capability = validateLinearHostCapability(input.operation, input.context, input.hostCapability);
|
|
235
|
+
if (!capability.valid) {
|
|
236
|
+
throw new Error('Linear mutation capability is unavailable.');
|
|
237
|
+
}
|
|
238
|
+
assertLinearMutationIdentity(input);
|
|
239
|
+
const fieldMask = normalizeLinearMutationFieldMask(input.operation, input.fieldMask, input.projection);
|
|
240
|
+
requireCurrentOutboundSafety(input.projection, input.outboundSafety);
|
|
241
|
+
const executionEvidence = {
|
|
242
|
+
capabilityEvidenceDigest: input.hostCapability.evidenceDigest,
|
|
243
|
+
projectionDigest: input.outboundSafety.projectionDigest,
|
|
244
|
+
outboundSafetyResultDigest: input.outboundSafety.resultDigest,
|
|
245
|
+
};
|
|
246
|
+
return {
|
|
247
|
+
previewDigest: linearMutationPreviewDigest({
|
|
248
|
+
operation: input.operation,
|
|
249
|
+
context: input.context,
|
|
250
|
+
bindingId: input.bindingId,
|
|
251
|
+
stableId: input.stableId ?? null,
|
|
252
|
+
provenance: input.provenance ?? null,
|
|
253
|
+
fieldMask,
|
|
254
|
+
projection: input.projection,
|
|
255
|
+
executionEvidence,
|
|
256
|
+
}),
|
|
257
|
+
executionEvidence,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
export function planLinearMutation(input) {
|
|
261
|
+
const preview = previewLinearMutation(input);
|
|
262
|
+
if (input.approvedPreviewDigest !== preview.previewDigest) {
|
|
263
|
+
throw new Error('Linear mutation approval does not match its preview.');
|
|
264
|
+
}
|
|
265
|
+
const fieldMask = normalizeLinearMutationFieldMask(input.operation, input.fieldMask, input.projection);
|
|
266
|
+
const actionDigest = linearMutationActionDigest({
|
|
267
|
+
operation: input.operation,
|
|
268
|
+
context: input.context,
|
|
269
|
+
bindingId: input.bindingId,
|
|
270
|
+
stableId: input.stableId ?? null,
|
|
271
|
+
provenance: input.provenance ?? null,
|
|
272
|
+
fieldMask,
|
|
273
|
+
projection: input.projection,
|
|
274
|
+
previewDigest: preview.previewDigest,
|
|
275
|
+
approvalDigest: input.approvedPreviewDigest,
|
|
276
|
+
executionEvidence: preview.executionEvidence,
|
|
277
|
+
});
|
|
278
|
+
return {
|
|
279
|
+
provider: 'linear',
|
|
280
|
+
operation: input.operation,
|
|
281
|
+
context: input.context,
|
|
282
|
+
intent: {
|
|
283
|
+
bindingId: input.bindingId,
|
|
284
|
+
stableId: input.stableId ?? null,
|
|
285
|
+
provenance: input.provenance ?? null,
|
|
286
|
+
fieldMask,
|
|
287
|
+
projection: { ...input.projection },
|
|
288
|
+
postconditions: { ...input.projection },
|
|
289
|
+
capabilityEvidenceDigest: input.hostCapability.evidenceDigest,
|
|
290
|
+
outboundSafety: {
|
|
291
|
+
projectionDigest: input.outboundSafety.projectionDigest,
|
|
292
|
+
resultDigest: input.outboundSafety.resultDigest,
|
|
293
|
+
},
|
|
294
|
+
outboundSafetyEvidence: { ...input.outboundSafety },
|
|
295
|
+
previewDigest: preview.previewDigest,
|
|
296
|
+
approvalDigest: input.approvedPreviewDigest,
|
|
297
|
+
actionDigest,
|
|
298
|
+
executionEvidence: {
|
|
299
|
+
...preview.executionEvidence,
|
|
300
|
+
previewDigest: preview.previewDigest,
|
|
301
|
+
approvalDigest: input.approvedPreviewDigest,
|
|
302
|
+
actionDigest,
|
|
303
|
+
},
|
|
304
|
+
readbackContract: {
|
|
305
|
+
pinned: true,
|
|
306
|
+
requireStableIdentity: true,
|
|
307
|
+
requireExactContext: true,
|
|
308
|
+
fields: fieldMask,
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
export function classifyLinearReadObservation(input) {
|
|
314
|
+
if (!validLinearReadAction(input.action)) {
|
|
315
|
+
return unavailableLinearRead('inaccessible', ['read-action-invalid']);
|
|
316
|
+
}
|
|
317
|
+
if (!Number.isFinite(Date.parse(input.observedAt))) {
|
|
318
|
+
throw new Error('Linear read classification requires a valid timestamp.');
|
|
319
|
+
}
|
|
320
|
+
const capability = validateLinearHostCapability('read', input.action.context, input.hostCapability, ['stable-identity', 'title', 'state', 'revision', 'team-context']);
|
|
321
|
+
if (!capability.valid ||
|
|
322
|
+
input.action.intent.capabilityEvidenceDigest !==
|
|
323
|
+
input.hostCapability.evidenceDigest) {
|
|
324
|
+
return unavailableLinearRead('inaccessible', [
|
|
325
|
+
...capability.reasons,
|
|
326
|
+
...(input.action.intent.capabilityEvidenceDigest !==
|
|
327
|
+
input.hostCapability.evidenceDigest
|
|
328
|
+
? ['capability-evidence-mismatch']
|
|
329
|
+
: []),
|
|
330
|
+
]);
|
|
331
|
+
}
|
|
332
|
+
if (input.outcome === 'temporary-failure') {
|
|
333
|
+
return unavailableLinearRead('temporarily-unavailable', [
|
|
334
|
+
'temporary-host-failure',
|
|
335
|
+
]);
|
|
336
|
+
}
|
|
337
|
+
if (input.outcome === 'not-found' || !input.observation) {
|
|
338
|
+
return unavailableLinearRead('inaccessible', [
|
|
339
|
+
input.outcome === 'not-found'
|
|
340
|
+
? 'absence-not-authoritative'
|
|
341
|
+
: 'read-observation-missing',
|
|
342
|
+
]);
|
|
343
|
+
}
|
|
344
|
+
const observation = input.observation;
|
|
345
|
+
if (observation.provider !== 'linear' ||
|
|
346
|
+
!contextsEqual(input.action.context, observation.context) ||
|
|
347
|
+
observation.capabilityEvidenceDigest !==
|
|
348
|
+
input.action.intent.capabilityEvidenceDigest ||
|
|
349
|
+
observation.fields.uuid !== input.action.intent.uuid) {
|
|
350
|
+
return unavailableLinearRead('inaccessible', [
|
|
351
|
+
'observation-attribution-mismatch',
|
|
352
|
+
]);
|
|
353
|
+
}
|
|
354
|
+
let issue;
|
|
355
|
+
try {
|
|
356
|
+
issue = normalizeLinearIssueObservation(observation);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return unavailableLinearRead('partial', ['read-observation-incomplete']);
|
|
360
|
+
}
|
|
361
|
+
if (issue.stableId !== input.action.intent.stableId) {
|
|
362
|
+
return unavailableLinearRead('inaccessible', [
|
|
363
|
+
'observation-identity-mismatch',
|
|
364
|
+
]);
|
|
365
|
+
}
|
|
366
|
+
const archived = observation.fields.archived === true;
|
|
367
|
+
const moved = typeof input.action.intent.currentIdentifier === 'string' &&
|
|
368
|
+
observation.fields.identifier !== input.action.intent.currentIdentifier &&
|
|
369
|
+
Array.isArray(observation.fields.historicalIdentifiers) &&
|
|
370
|
+
observation.fields.historicalIdentifiers.includes(input.action.intent.currentIdentifier);
|
|
371
|
+
return {
|
|
372
|
+
classification: archived ? 'archived' : moved ? 'moved' : 'current',
|
|
373
|
+
issue,
|
|
374
|
+
preservePriorEvidence: false,
|
|
375
|
+
reasons: [],
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
export function validateLinearDiscussionReadObservation(input) {
|
|
379
|
+
const invalid = (reasons) => ({
|
|
380
|
+
classification: 'invalid',
|
|
381
|
+
page: null,
|
|
382
|
+
persistable: false,
|
|
383
|
+
reasons,
|
|
384
|
+
});
|
|
385
|
+
if (!validLinearDiscussionAction(input.action) ||
|
|
386
|
+
!contextsEqual(input.action.context, input.observation.context) ||
|
|
387
|
+
input.observation.provider !== 'linear' ||
|
|
388
|
+
input.observation.stableId !== input.action.intent.stableId ||
|
|
389
|
+
input.observation.capabilityEvidenceDigest !==
|
|
390
|
+
input.action.intent.capabilityEvidenceDigest ||
|
|
391
|
+
input.observation.requestedCursor !== input.action.intent.cursor) {
|
|
392
|
+
return invalid(['discussion-observation-attribution-mismatch']);
|
|
393
|
+
}
|
|
394
|
+
const capability = validateLinearHostCapability('read-discussion', input.action.context, input.hostCapability);
|
|
395
|
+
if (!capability.valid ||
|
|
396
|
+
input.hostCapability.evidenceDigest !==
|
|
397
|
+
input.action.intent.capabilityEvidenceDigest) {
|
|
398
|
+
return invalid([
|
|
399
|
+
...capability.reasons,
|
|
400
|
+
...(input.hostCapability.evidenceDigest !==
|
|
401
|
+
input.action.intent.capabilityEvidenceDigest
|
|
402
|
+
? ['capability-evidence-mismatch']
|
|
403
|
+
: []),
|
|
404
|
+
]);
|
|
405
|
+
}
|
|
406
|
+
if (input.observation.availability !== 'available') {
|
|
407
|
+
return {
|
|
408
|
+
classification: input.observation.availability,
|
|
409
|
+
page: null,
|
|
410
|
+
persistable: false,
|
|
411
|
+
reasons: [input.observation.availability],
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const contract = input.action.intent.resultContract;
|
|
415
|
+
const maxItems = Number(contract.maxItems);
|
|
416
|
+
const pageBytes = Buffer.byteLength(input.observation.nextCursor ?? '', 'utf8') +
|
|
417
|
+
input.observation.items.reduce((total, item) => total +
|
|
418
|
+
Buffer.byteLength(item.id, 'utf8') +
|
|
419
|
+
Buffer.byteLength(item.body, 'utf8'), 0);
|
|
420
|
+
if (!Number.isInteger(maxItems) ||
|
|
421
|
+
input.observation.items.length > maxItems ||
|
|
422
|
+
pageBytes > LINEAR_DISCUSSION_LIMITS.maxPageBytes ||
|
|
423
|
+
input.observation.items.some((item) => !item.id ||
|
|
424
|
+
Buffer.byteLength(item.id, 'utf8') >
|
|
425
|
+
LINEAR_DISCUSSION_LIMITS.maxIdBytes ||
|
|
426
|
+
typeof item.body !== 'string' ||
|
|
427
|
+
Buffer.byteLength(item.body, 'utf8') >
|
|
428
|
+
LINEAR_DISCUSSION_LIMITS.maxBodyBytes ||
|
|
429
|
+
!Number.isFinite(Date.parse(item.observedAt))) ||
|
|
430
|
+
(input.observation.nextCursor !== null &&
|
|
431
|
+
(typeof input.observation.nextCursor !== 'string' ||
|
|
432
|
+
Buffer.byteLength(input.observation.nextCursor, 'utf8') >
|
|
433
|
+
LINEAR_DISCUSSION_LIMITS.maxCursorBytes))) {
|
|
434
|
+
return invalid(['discussion-page-invalid']);
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
classification: 'page',
|
|
438
|
+
page: {
|
|
439
|
+
items: input.observation.items.map((item) => {
|
|
440
|
+
const contentSuppressed = containsSensitiveContentSignal(item.body);
|
|
441
|
+
return {
|
|
442
|
+
...item,
|
|
443
|
+
body: contentSuppressed ? WHOLE_FIELD_SUPPRESSION_MARKER : item.body,
|
|
444
|
+
contentSuppressed,
|
|
445
|
+
};
|
|
446
|
+
}),
|
|
447
|
+
nextCursor: input.observation.nextCursor,
|
|
448
|
+
},
|
|
449
|
+
persistable: false,
|
|
450
|
+
reasons: [],
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
export function verifyLinearMutationObservation(input) {
|
|
454
|
+
const fields = mutationVerificationFields(input.action);
|
|
455
|
+
const result = (classification, reason, fieldResults = fields.map((field) => ({
|
|
456
|
+
field,
|
|
457
|
+
status: 'unavailable',
|
|
458
|
+
}))) => ({
|
|
459
|
+
classification,
|
|
460
|
+
reason,
|
|
461
|
+
fields: fieldResults,
|
|
462
|
+
retryAllowed: false,
|
|
463
|
+
});
|
|
464
|
+
if (input.action.provider !== 'linear' ||
|
|
465
|
+
!['create', 'update', 'transition', 'annotate'].includes(input.action.operation) ||
|
|
466
|
+
!validLinearMutationAction(input.action) ||
|
|
467
|
+
input.attempt.count !== 1 ||
|
|
468
|
+
input.attempt.capabilityEvidenceDigest !==
|
|
469
|
+
input.action.intent.capabilityEvidenceDigest) {
|
|
470
|
+
return result('uncertain', 'mutation-attribution-invalid');
|
|
471
|
+
}
|
|
472
|
+
if (input.attempt.outcome === 'rejected') {
|
|
473
|
+
return result('rejected', 'provider-rejected-before-acceptance');
|
|
474
|
+
}
|
|
475
|
+
if (input.attempt.outcome === 'unknown' || !input.readback) {
|
|
476
|
+
return result('uncertain', 'authoritative-readback-required');
|
|
477
|
+
}
|
|
478
|
+
const capability = validateLinearHostCapability(input.action.operation, input.action.context, input.hostCapability);
|
|
479
|
+
if (!capability.valid ||
|
|
480
|
+
input.hostCapability.evidenceDigest !==
|
|
481
|
+
input.action.intent.capabilityEvidenceDigest ||
|
|
482
|
+
input.readback.provider !== 'linear' ||
|
|
483
|
+
!contextsEqual(input.action.context, input.readback.context) ||
|
|
484
|
+
input.readback.capabilityEvidenceDigest !==
|
|
485
|
+
input.action.intent.capabilityEvidenceDigest ||
|
|
486
|
+
!semanticValuesEqual(input.readback.fields.mutationEvidence, input.action.intent.executionEvidence)) {
|
|
487
|
+
return result('uncertain', 'pinned-readback-evidence-mismatch');
|
|
488
|
+
}
|
|
489
|
+
let issue;
|
|
490
|
+
try {
|
|
491
|
+
issue = normalizeLinearIssueObservation(input.readback);
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
return result('uncertain', 'authoritative-readback-invalid');
|
|
495
|
+
}
|
|
496
|
+
if (input.action.operation !== 'create' &&
|
|
497
|
+
issue.stableId !== input.action.intent.stableId) {
|
|
498
|
+
return result('uncertain', 'pinned-readback-identity-mismatch');
|
|
499
|
+
}
|
|
500
|
+
if (input.action.operation === 'create' &&
|
|
501
|
+
!semanticValuesEqual(input.readback.fields.createProvenance, input.action.intent.provenance)) {
|
|
502
|
+
return result('uncertain', 'create-provenance-mismatch');
|
|
503
|
+
}
|
|
504
|
+
const postconditions = input.action.intent.postconditions;
|
|
505
|
+
const fieldResults = fields.map((field) => {
|
|
506
|
+
const expected = postconditions[field];
|
|
507
|
+
const observed = field === 'description'
|
|
508
|
+
? issue.description
|
|
509
|
+
: field === 'status'
|
|
510
|
+
? issue.status
|
|
511
|
+
: field === 'annotation'
|
|
512
|
+
? issue.extensions.annotations
|
|
513
|
+
: issue[field];
|
|
514
|
+
const verified = field === 'annotation' && Array.isArray(observed)
|
|
515
|
+
? observed.includes(expected)
|
|
516
|
+
: semanticValuesEqual(observed, expected);
|
|
517
|
+
return { field, status: verified ? 'verified' : 'mismatch' };
|
|
518
|
+
});
|
|
519
|
+
return result(fieldResults.every((field) => field.status === 'verified')
|
|
520
|
+
? 'verified'
|
|
521
|
+
: 'partial', fieldResults.every((field) => field.status === 'verified')
|
|
522
|
+
? 'authoritative-readback-matched'
|
|
523
|
+
: 'authoritative-readback-postcondition-mismatch', fieldResults);
|
|
524
|
+
}
|
|
525
|
+
export function planLinearDuplicateSearch(input) {
|
|
526
|
+
const capability = validateLinearHostCapability('search-duplicates', input.context, input.hostCapability);
|
|
527
|
+
if (!capability.valid) {
|
|
528
|
+
throw new Error('Linear duplicate search capability is unavailable.');
|
|
529
|
+
}
|
|
530
|
+
if (!input.provenanceToken ||
|
|
531
|
+
input.provenanceToken.length > 512 ||
|
|
532
|
+
!input.reservedBindingId ||
|
|
533
|
+
input.reservedBindingId.length > 128 ||
|
|
534
|
+
!Number.isInteger(input.maxResults) ||
|
|
535
|
+
input.maxResults < 1 ||
|
|
536
|
+
input.maxResults > 100 ||
|
|
537
|
+
input.historicalIdentifiers.length > 64 ||
|
|
538
|
+
new Set(input.historicalIdentifiers).size !==
|
|
539
|
+
input.historicalIdentifiers.length ||
|
|
540
|
+
input.historicalIdentifiers.some((identifier) => !IDENTIFIER.test(identifier))) {
|
|
541
|
+
throw new Error('Linear duplicate search bounds are invalid.');
|
|
542
|
+
}
|
|
543
|
+
const query = {
|
|
544
|
+
provenanceToken: input.provenanceToken,
|
|
545
|
+
reservedBindingId: input.reservedBindingId,
|
|
546
|
+
historicalIdentifiers: [...input.historicalIdentifiers],
|
|
547
|
+
workspaceId: input.context.workspaceId,
|
|
548
|
+
teamId: input.context.teamId,
|
|
549
|
+
};
|
|
550
|
+
return {
|
|
551
|
+
provider: 'linear',
|
|
552
|
+
operation: 'search-duplicates',
|
|
553
|
+
context: input.context,
|
|
554
|
+
intent: {
|
|
555
|
+
query,
|
|
556
|
+
queryDigest: semanticDigest(query),
|
|
557
|
+
resultContract: {
|
|
558
|
+
maxResults: input.maxResults,
|
|
559
|
+
classifications: ['no-match', 'one-match', 'ambiguous'],
|
|
560
|
+
requireStableUuid: true,
|
|
561
|
+
requireExactContext: true,
|
|
562
|
+
matchStatus: 'evidence-until-identity-and-context-verified',
|
|
563
|
+
},
|
|
564
|
+
capabilityEvidenceDigest: input.hostCapability.evidenceDigest,
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
export function validateLinearDuplicateSearchObservation(input) {
|
|
569
|
+
const result = (accepted, classification, stableId, reasons) => ({
|
|
570
|
+
accepted,
|
|
571
|
+
classification,
|
|
572
|
+
stableId,
|
|
573
|
+
reasons,
|
|
574
|
+
});
|
|
575
|
+
if (!validLinearDuplicateAction(input.action) ||
|
|
576
|
+
input.observation.provider !== 'linear' ||
|
|
577
|
+
!contextsEqual(input.action.context, input.observation.context) ||
|
|
578
|
+
input.observation.capabilityEvidenceDigest !==
|
|
579
|
+
input.action.intent.capabilityEvidenceDigest ||
|
|
580
|
+
input.observation.queryDigest !== input.action.intent.queryDigest ||
|
|
581
|
+
!Number.isFinite(Date.parse(input.observation.observedAt))) {
|
|
582
|
+
return result(false, 'invalid', null, ['observation-attribution-mismatch']);
|
|
583
|
+
}
|
|
584
|
+
const capability = validateLinearHostCapability('search-duplicates', input.action.context, input.hostCapability);
|
|
585
|
+
if (input.hostCapability.evidenceDigest !==
|
|
586
|
+
input.action.intent.capabilityEvidenceDigest) {
|
|
587
|
+
return result(false, 'invalid', null, ['capability-evidence-mismatch']);
|
|
588
|
+
}
|
|
589
|
+
if (input.observation.availability === 'unavailable' ||
|
|
590
|
+
input.hostCapability.availability !== 'available') {
|
|
591
|
+
return result(false, 'unavailable', null, [
|
|
592
|
+
...capability.reasons,
|
|
593
|
+
'search-unavailable',
|
|
594
|
+
]);
|
|
595
|
+
}
|
|
596
|
+
if (!capability.valid) {
|
|
597
|
+
return result(false, 'invalid', null, capability.reasons);
|
|
598
|
+
}
|
|
599
|
+
const resultContract = input.action.intent.resultContract;
|
|
600
|
+
const query = input.action.intent.query;
|
|
601
|
+
if (!isRecord(resultContract) || !isRecord(query)) {
|
|
602
|
+
return result(false, 'invalid', null, ['action-contract-invalid']);
|
|
603
|
+
}
|
|
604
|
+
const maxResults = Number(resultContract.maxResults);
|
|
605
|
+
if (!Number.isInteger(maxResults) ||
|
|
606
|
+
maxResults < 1 ||
|
|
607
|
+
maxResults > 100 ||
|
|
608
|
+
input.observation.results.length > maxResults) {
|
|
609
|
+
return result(false, 'invalid', null, ['result-bound-exceeded']);
|
|
610
|
+
}
|
|
611
|
+
if (input.observation.results.length === 0) {
|
|
612
|
+
return result(true, 'no-match', null, []);
|
|
613
|
+
}
|
|
614
|
+
if (input.observation.results.some((candidate) => !validLinearDuplicateCandidateBounds(candidate))) {
|
|
615
|
+
return result(false, 'invalid', null, ['candidate-bound-invalid']);
|
|
616
|
+
}
|
|
617
|
+
if (input.observation.results.length > 1) {
|
|
618
|
+
return result(false, 'ambiguous', null, ['multiple-candidates']);
|
|
619
|
+
}
|
|
620
|
+
const candidate = input.observation.results[0];
|
|
621
|
+
const historicalIdentifiers = Array.isArray(query.historicalIdentifiers)
|
|
622
|
+
? query.historicalIdentifiers.filter((identifier) => typeof identifier === 'string')
|
|
623
|
+
: [];
|
|
624
|
+
const matchEvidenceValid = candidate.matchedBy === 'provenance'
|
|
625
|
+
? candidate.matchedProvenanceToken === query.provenanceToken
|
|
626
|
+
: candidate.matchedBy === 'reserved-binding'
|
|
627
|
+
? candidate.matchedReservedBindingId === query.reservedBindingId
|
|
628
|
+
: typeof candidate.matchedIdentifier === 'string' &&
|
|
629
|
+
historicalIdentifiers.includes(candidate.matchedIdentifier) &&
|
|
630
|
+
candidate.identifiers.includes(candidate.matchedIdentifier);
|
|
631
|
+
const expectedStableId = canonicalLinearStableId(String(query.workspaceId), candidate.uuid);
|
|
632
|
+
if (!candidate.stableIdentityVerified ||
|
|
633
|
+
!candidate.contextVerified ||
|
|
634
|
+
candidate.stableId !== expectedStableId ||
|
|
635
|
+
!contextsEqual(candidate.context, input.action.context) ||
|
|
636
|
+
candidate.context.workspaceId !== query.workspaceId ||
|
|
637
|
+
candidate.context.teamId !== query.teamId ||
|
|
638
|
+
!matchEvidenceValid) {
|
|
639
|
+
return result(false, 'ambiguous', null, ['candidate-not-fully-verified']);
|
|
640
|
+
}
|
|
641
|
+
return result(true, 'one-verified-match', candidate.stableId, []);
|
|
642
|
+
}
|
|
643
|
+
export const linearAdapter = {
|
|
644
|
+
provider: 'linear',
|
|
645
|
+
normalize: normalizeLinearIssueObservation,
|
|
646
|
+
plan(operation, input) {
|
|
647
|
+
if (['create', 'update', 'transition', 'annotate'].includes(operation)) {
|
|
648
|
+
if (typeof input.operation !== 'string') {
|
|
649
|
+
throw new Error('Linear adapter requires complete mutation input.');
|
|
650
|
+
}
|
|
651
|
+
if (input.operation !== operation) {
|
|
652
|
+
throw new Error('Linear mutation operation selector does not match input operation.');
|
|
653
|
+
}
|
|
654
|
+
if (!isCompleteLinearMutationInput(input, operation)) {
|
|
655
|
+
throw new Error('Linear adapter requires complete mutation input.');
|
|
656
|
+
}
|
|
657
|
+
return planLinearMutation(input);
|
|
658
|
+
}
|
|
659
|
+
if (operation === 'read') {
|
|
660
|
+
if (!isCompleteLinearReadInput(input)) {
|
|
661
|
+
throw new Error('Linear adapter requires complete read input.');
|
|
662
|
+
}
|
|
663
|
+
return planLinearRead(input);
|
|
664
|
+
}
|
|
665
|
+
if (operation === 'read-discussion') {
|
|
666
|
+
if (!isCompleteLinearDiscussionInput(input)) {
|
|
667
|
+
throw new Error('Linear adapter requires complete discussion input.');
|
|
668
|
+
}
|
|
669
|
+
return planLinearDiscussionRead(input);
|
|
670
|
+
}
|
|
671
|
+
if (operation === 'search-duplicates') {
|
|
672
|
+
if (!isCompleteLinearDuplicateSearchInput(input)) {
|
|
673
|
+
throw new Error('Linear adapter requires complete duplicate search input.');
|
|
674
|
+
}
|
|
675
|
+
return planLinearDuplicateSearch(input);
|
|
676
|
+
}
|
|
677
|
+
throw new Error(`Linear adapter does not support '${operation}' yet.`);
|
|
678
|
+
},
|
|
679
|
+
validateObservation(action, observation) {
|
|
680
|
+
if (isLinearSpecializedOperation(action.operation)) {
|
|
681
|
+
return {
|
|
682
|
+
valid: false,
|
|
683
|
+
reasons: [`typed-validator-required:${action.operation}`],
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
if (action.operation === 'read' && !validLinearReadAction(action)) {
|
|
687
|
+
return { valid: false, reasons: ['action-evidence-invalid'] };
|
|
688
|
+
}
|
|
689
|
+
if (['create', 'update', 'transition', 'annotate'].includes(action.operation) &&
|
|
690
|
+
!validLinearMutationAction(action)) {
|
|
691
|
+
return { valid: false, reasons: ['action-evidence-invalid'] };
|
|
692
|
+
}
|
|
693
|
+
if (action.provider !== 'linear' ||
|
|
694
|
+
observation.provider !== 'linear' ||
|
|
695
|
+
!contextsEqual(action.context, observation.context)) {
|
|
696
|
+
return { valid: false, reasons: ['observation-context-mismatch'] };
|
|
697
|
+
}
|
|
698
|
+
const capability = parseLinearHostCapability(observation.fields.hostCapability);
|
|
699
|
+
if (!capability) {
|
|
700
|
+
return { valid: false, reasons: ['capability-evidence-missing'] };
|
|
701
|
+
}
|
|
702
|
+
const validation = validateLinearHostCapability(action.operation, action.context, capability, action.operation === 'read'
|
|
703
|
+
? ['stable-identity', 'title', 'state', 'revision', 'team-context']
|
|
704
|
+
: []);
|
|
705
|
+
if (!validation.valid)
|
|
706
|
+
return validation;
|
|
707
|
+
if (observation.capabilityEvidenceDigest !==
|
|
708
|
+
action.intent.capabilityEvidenceDigest ||
|
|
709
|
+
capability.evidenceDigest !== action.intent.capabilityEvidenceDigest) {
|
|
710
|
+
return { valid: false, reasons: ['capability-evidence-mismatch'] };
|
|
711
|
+
}
|
|
712
|
+
if (action.operation === 'read') {
|
|
713
|
+
try {
|
|
714
|
+
const issue = normalizeLinearIssueObservation(observation);
|
|
715
|
+
if (issue.extensions.uuid !== String(action.intent.uuid).toLowerCase() ||
|
|
716
|
+
issue.stableId !== action.intent.stableId) {
|
|
717
|
+
return { valid: false, reasons: ['observation-identity-mismatch'] };
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
return { valid: false, reasons: ['observation-invalid'] };
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (['create', 'update', 'transition', 'annotate'].includes(action.operation)) {
|
|
725
|
+
if (!semanticValuesEqual(observation.fields.mutationEvidence, action.intent.executionEvidence)) {
|
|
726
|
+
return { valid: false, reasons: ['mutation-evidence-mismatch'] };
|
|
727
|
+
}
|
|
728
|
+
if (action.operation === 'create' &&
|
|
729
|
+
!semanticValuesEqual(observation.fields.createProvenance, action.intent.provenance)) {
|
|
730
|
+
return { valid: false, reasons: ['create-provenance-mismatch'] };
|
|
731
|
+
}
|
|
732
|
+
try {
|
|
733
|
+
const issue = normalizeLinearIssueObservation(observation);
|
|
734
|
+
if (action.operation !== 'create' &&
|
|
735
|
+
issue.stableId !== action.intent.stableId) {
|
|
736
|
+
return { valid: false, reasons: ['observation-identity-mismatch'] };
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
return { valid: false, reasons: ['observation-invalid'] };
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return { valid: true, reasons: [] };
|
|
744
|
+
},
|
|
745
|
+
verificationFields(action) {
|
|
746
|
+
if (isLinearSpecializedOperation(action.operation)) {
|
|
747
|
+
return [`typed:${action.operation}`];
|
|
748
|
+
}
|
|
749
|
+
if (action.operation === 'read')
|
|
750
|
+
return ['stable-identity'];
|
|
751
|
+
return mutationVerificationFields(action);
|
|
752
|
+
},
|
|
753
|
+
verify(action, issue) {
|
|
754
|
+
if (isLinearSpecializedOperation(action.operation)) {
|
|
755
|
+
return [
|
|
756
|
+
{
|
|
757
|
+
field: `typed:${action.operation}`,
|
|
758
|
+
status: 'unavailable',
|
|
759
|
+
},
|
|
760
|
+
];
|
|
761
|
+
}
|
|
762
|
+
if (action.operation === 'read') {
|
|
763
|
+
if (!validLinearReadAction(action) ||
|
|
764
|
+
issue.provider !== 'linear' ||
|
|
765
|
+
!contextsEqual(action.context, issue.context) ||
|
|
766
|
+
issue.extensions.capabilityEvidenceDigest !==
|
|
767
|
+
action.intent.capabilityEvidenceDigest) {
|
|
768
|
+
return [{ field: 'stable-identity', status: 'unavailable' }];
|
|
769
|
+
}
|
|
770
|
+
return [
|
|
771
|
+
{
|
|
772
|
+
field: 'stable-identity',
|
|
773
|
+
status: issue.stableId === action.intent.stableId ? 'verified' : 'mismatch',
|
|
774
|
+
},
|
|
775
|
+
];
|
|
776
|
+
}
|
|
777
|
+
const fields = mutationVerificationFields(action);
|
|
778
|
+
if (!validLinearMutationAction(action) ||
|
|
779
|
+
issue.provider !== 'linear' ||
|
|
780
|
+
!contextsEqual(action.context, issue.context) ||
|
|
781
|
+
(action.operation !== 'create' &&
|
|
782
|
+
issue.stableId !== action.intent.stableId) ||
|
|
783
|
+
issue.extensions.capabilityEvidenceDigest !==
|
|
784
|
+
action.intent.capabilityEvidenceDigest ||
|
|
785
|
+
!semanticValuesEqual(issue.extensions.mutationEvidence, action.intent.executionEvidence) ||
|
|
786
|
+
(action.operation === 'create' &&
|
|
787
|
+
!semanticValuesEqual(issue.extensions.createProvenance, action.intent.provenance))) {
|
|
788
|
+
return fields.map((field) => ({
|
|
789
|
+
field,
|
|
790
|
+
status: 'unavailable',
|
|
791
|
+
}));
|
|
792
|
+
}
|
|
793
|
+
const postconditions = action.intent.postconditions;
|
|
794
|
+
return fields.map((field) => {
|
|
795
|
+
const observed = field === 'description'
|
|
796
|
+
? issue.description
|
|
797
|
+
: field === 'status'
|
|
798
|
+
? issue.status
|
|
799
|
+
: field === 'annotation'
|
|
800
|
+
? issue.extensions.annotations
|
|
801
|
+
: issue[field];
|
|
802
|
+
return {
|
|
803
|
+
field,
|
|
804
|
+
status: field === 'annotation' && Array.isArray(observed)
|
|
805
|
+
? observed.includes(postconditions[field])
|
|
806
|
+
? 'verified'
|
|
807
|
+
: 'mismatch'
|
|
808
|
+
: semanticValuesEqual(observed, postconditions[field])
|
|
809
|
+
? 'verified'
|
|
810
|
+
: 'mismatch',
|
|
811
|
+
};
|
|
812
|
+
});
|
|
813
|
+
},
|
|
814
|
+
};
|
|
815
|
+
function canonicalLinearStableId(workspaceId, uuid) {
|
|
816
|
+
return `linear:${workspaceId}:${uuid.toLowerCase()}`;
|
|
817
|
+
}
|
|
818
|
+
function hasPinnedLinearContext(context) {
|
|
819
|
+
return ['accountId', 'workspaceId', 'teamId'].every((key) => typeof context[key] === 'string' && context[key].length > 0);
|
|
820
|
+
}
|
|
821
|
+
function assertLinearMutationIdentity(input) {
|
|
822
|
+
if (!hasPinnedLinearContext(input.context) || !input.bindingId) {
|
|
823
|
+
throw new Error('Linear mutation requires a binding and pinned context.');
|
|
824
|
+
}
|
|
825
|
+
if (input.operation === 'create') {
|
|
826
|
+
if (input.stableId !== undefined ||
|
|
827
|
+
input.provenance?.bindingId !== input.bindingId ||
|
|
828
|
+
!input.provenance.origin) {
|
|
829
|
+
throw new Error('Linear create requires exact creation provenance.');
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
else if (!input.stableId) {
|
|
833
|
+
throw new Error('Linear mutation requires a stable issue identity.');
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
function unavailableLinearRead(classification, reasons) {
|
|
837
|
+
return {
|
|
838
|
+
classification,
|
|
839
|
+
issue: null,
|
|
840
|
+
preservePriorEvidence: true,
|
|
841
|
+
reasons,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
function normalizeLinearMutationFieldMask(operation, fieldMask, projection) {
|
|
845
|
+
const allowedByOperation = {
|
|
846
|
+
create: ['title', 'description', 'priority'],
|
|
847
|
+
update: ['title', 'description', 'priority'],
|
|
848
|
+
transition: ['status'],
|
|
849
|
+
annotate: ['annotation'],
|
|
850
|
+
};
|
|
851
|
+
if (fieldMask.length === 0 ||
|
|
852
|
+
new Set(fieldMask).size !== fieldMask.length ||
|
|
853
|
+
fieldMask.some((field) => !allowedByOperation[operation].includes(field)) ||
|
|
854
|
+
Object.keys(projection).length !== fieldMask.length ||
|
|
855
|
+
Object.keys(projection).some((field) => !fieldMask.includes(field))) {
|
|
856
|
+
throw new Error('Linear mutation projection or field mask is invalid.');
|
|
857
|
+
}
|
|
858
|
+
return [...fieldMask];
|
|
859
|
+
}
|
|
860
|
+
function assertLinearObservation(observation) {
|
|
861
|
+
if (observation.provider !== 'linear') {
|
|
862
|
+
throw new Error('Expected a Linear observation.');
|
|
863
|
+
}
|
|
864
|
+
if (!observation.capabilityEvidenceDigest) {
|
|
865
|
+
throw new Error('Linear observation capability evidence is missing.');
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
function requiredContext(observation, key) {
|
|
869
|
+
const value = observation.context[key];
|
|
870
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
871
|
+
throw new Error(`Linear observation context '${key}' is missing.`);
|
|
872
|
+
}
|
|
873
|
+
return value;
|
|
874
|
+
}
|
|
875
|
+
function requiredString(fields, key) {
|
|
876
|
+
const value = fields[key];
|
|
877
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
878
|
+
throw new Error(`Linear observation field '${key}' is invalid.`);
|
|
879
|
+
}
|
|
880
|
+
return value;
|
|
881
|
+
}
|
|
882
|
+
function nullableString(value) {
|
|
883
|
+
if (value === null || value === undefined)
|
|
884
|
+
return null;
|
|
885
|
+
if (typeof value !== 'string') {
|
|
886
|
+
throw new Error('Linear optional text field is invalid.');
|
|
887
|
+
}
|
|
888
|
+
return value;
|
|
889
|
+
}
|
|
890
|
+
function requiredBoolean(fields, key) {
|
|
891
|
+
const value = fields[key];
|
|
892
|
+
if (typeof value !== 'boolean') {
|
|
893
|
+
throw new Error(`Linear observation field '${key}' is invalid.`);
|
|
894
|
+
}
|
|
895
|
+
return value;
|
|
896
|
+
}
|
|
897
|
+
function stringArray(value) {
|
|
898
|
+
if (value === undefined)
|
|
899
|
+
return [];
|
|
900
|
+
if (!Array.isArray(value) ||
|
|
901
|
+
value.some((entry) => typeof entry !== 'string')) {
|
|
902
|
+
throw new Error('Linear observation alias list is invalid.');
|
|
903
|
+
}
|
|
904
|
+
return uniqueStrings(value);
|
|
905
|
+
}
|
|
906
|
+
function uniqueStrings(values) {
|
|
907
|
+
return [...new Set(values.filter((value) => value.length > 0))];
|
|
908
|
+
}
|
|
909
|
+
function suppressInboundText(value, field, suppressedFields) {
|
|
910
|
+
if (!containsSensitiveContentSignal(value))
|
|
911
|
+
return value;
|
|
912
|
+
suppressedFields.push(field);
|
|
913
|
+
return WHOLE_FIELD_SUPPRESSION_MARKER;
|
|
914
|
+
}
|
|
915
|
+
function suppressNullableInboundText(value, field, suppressedFields) {
|
|
916
|
+
const normalized = nullableString(value);
|
|
917
|
+
return normalized === null
|
|
918
|
+
? null
|
|
919
|
+
: suppressInboundText(normalized, field, suppressedFields);
|
|
920
|
+
}
|
|
921
|
+
function isBoundedExtensionValue(value) {
|
|
922
|
+
return ((typeof value === 'string' && value.length <= 256) ||
|
|
923
|
+
(typeof value === 'number' && Number.isFinite(value)) ||
|
|
924
|
+
typeof value === 'boolean' ||
|
|
925
|
+
value === null);
|
|
926
|
+
}
|
|
927
|
+
function mutationVerificationFields(action) {
|
|
928
|
+
return Array.isArray(action.intent.fieldMask)
|
|
929
|
+
? action.intent.fieldMask.filter((field) => typeof field === 'string')
|
|
930
|
+
: [];
|
|
931
|
+
}
|
|
932
|
+
function validLinearMutationAction(action) {
|
|
933
|
+
if (action.provider !== 'linear' ||
|
|
934
|
+
!['create', 'update', 'transition', 'annotate'].includes(action.operation) ||
|
|
935
|
+
!hasPinnedLinearContext(action.context)) {
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
const operation = action.operation;
|
|
939
|
+
const intent = action.intent;
|
|
940
|
+
const projection = intent.projection;
|
|
941
|
+
const postconditions = intent.postconditions;
|
|
942
|
+
const outboundSafety = intent.outboundSafety;
|
|
943
|
+
const outboundSafetyEvidence = intent.outboundSafetyEvidence;
|
|
944
|
+
const executionEvidence = intent.executionEvidence;
|
|
945
|
+
if (typeof intent.bindingId !== 'string' ||
|
|
946
|
+
intent.bindingId.length === 0 ||
|
|
947
|
+
intent.bindingId.length > 128 ||
|
|
948
|
+
!Array.isArray(intent.fieldMask) ||
|
|
949
|
+
intent.fieldMask.some((field) => typeof field !== 'string') ||
|
|
950
|
+
!isRecord(projection) ||
|
|
951
|
+
!isRecord(postconditions) ||
|
|
952
|
+
!semanticValuesEqual(projection, postconditions) ||
|
|
953
|
+
typeof intent.capabilityEvidenceDigest !== 'string' ||
|
|
954
|
+
intent.capabilityEvidenceDigest.length === 0 ||
|
|
955
|
+
!isRecord(outboundSafety) ||
|
|
956
|
+
!isRecord(outboundSafetyEvidence) ||
|
|
957
|
+
!isRecord(executionEvidence) ||
|
|
958
|
+
typeof intent.previewDigest !== 'string' ||
|
|
959
|
+
intent.approvalDigest !== intent.previewDigest ||
|
|
960
|
+
typeof intent.actionDigest !== 'string') {
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
const stableId = intent.stableId;
|
|
964
|
+
const provenance = intent.provenance;
|
|
965
|
+
if (operation === 'create') {
|
|
966
|
+
if (stableId !== null ||
|
|
967
|
+
!isRecord(provenance) ||
|
|
968
|
+
provenance.bindingId !== intent.bindingId ||
|
|
969
|
+
typeof provenance.origin !== 'string' ||
|
|
970
|
+
provenance.origin.length === 0 ||
|
|
971
|
+
provenance.origin.length > 512 ||
|
|
972
|
+
Object.keys(provenance).length !== 2) {
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
else if (provenance !== null ||
|
|
977
|
+
typeof stableId !== 'string' ||
|
|
978
|
+
!validLinearStableIdForContext(stableId, action.context)) {
|
|
979
|
+
return false;
|
|
980
|
+
}
|
|
981
|
+
let fieldMask;
|
|
982
|
+
try {
|
|
983
|
+
fieldMask = normalizeLinearMutationFieldMask(operation, intent.fieldMask, projection);
|
|
984
|
+
requireCurrentOutboundSafety(projection, outboundSafetyEvidence);
|
|
985
|
+
}
|
|
986
|
+
catch {
|
|
987
|
+
return false;
|
|
988
|
+
}
|
|
989
|
+
const safety = outboundSafetyEvidence;
|
|
990
|
+
if (outboundSafety.projectionDigest !== safety.projectionDigest ||
|
|
991
|
+
outboundSafety.resultDigest !== safety.resultDigest ||
|
|
992
|
+
semanticDigest(projection) !== safety.projectionDigest) {
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
const baseExecutionEvidence = {
|
|
996
|
+
capabilityEvidenceDigest: intent.capabilityEvidenceDigest,
|
|
997
|
+
projectionDigest: safety.projectionDigest,
|
|
998
|
+
outboundSafetyResultDigest: safety.resultDigest,
|
|
999
|
+
};
|
|
1000
|
+
const expectedPreviewDigest = linearMutationPreviewDigest({
|
|
1001
|
+
operation,
|
|
1002
|
+
context: action.context,
|
|
1003
|
+
bindingId: intent.bindingId,
|
|
1004
|
+
stableId: stableId,
|
|
1005
|
+
provenance: provenance,
|
|
1006
|
+
fieldMask,
|
|
1007
|
+
projection,
|
|
1008
|
+
executionEvidence: baseExecutionEvidence,
|
|
1009
|
+
});
|
|
1010
|
+
if (intent.previewDigest !== expectedPreviewDigest)
|
|
1011
|
+
return false;
|
|
1012
|
+
const expectedActionDigest = linearMutationActionDigest({
|
|
1013
|
+
operation,
|
|
1014
|
+
context: action.context,
|
|
1015
|
+
bindingId: intent.bindingId,
|
|
1016
|
+
stableId: stableId,
|
|
1017
|
+
provenance: provenance,
|
|
1018
|
+
fieldMask,
|
|
1019
|
+
projection,
|
|
1020
|
+
previewDigest: expectedPreviewDigest,
|
|
1021
|
+
approvalDigest: intent.approvalDigest,
|
|
1022
|
+
executionEvidence: baseExecutionEvidence,
|
|
1023
|
+
});
|
|
1024
|
+
const expectedExecutionEvidence = {
|
|
1025
|
+
...baseExecutionEvidence,
|
|
1026
|
+
previewDigest: expectedPreviewDigest,
|
|
1027
|
+
approvalDigest: intent.approvalDigest,
|
|
1028
|
+
actionDigest: expectedActionDigest,
|
|
1029
|
+
};
|
|
1030
|
+
const expectedReadbackContract = {
|
|
1031
|
+
pinned: true,
|
|
1032
|
+
requireStableIdentity: true,
|
|
1033
|
+
requireExactContext: true,
|
|
1034
|
+
fields: fieldMask,
|
|
1035
|
+
};
|
|
1036
|
+
return (intent.actionDigest === expectedActionDigest &&
|
|
1037
|
+
semanticValuesEqual(executionEvidence, expectedExecutionEvidence) &&
|
|
1038
|
+
semanticValuesEqual(intent.readbackContract, expectedReadbackContract));
|
|
1039
|
+
}
|
|
1040
|
+
function linearMutationPreviewDigest(input) {
|
|
1041
|
+
return semanticDigest({
|
|
1042
|
+
provider: 'linear',
|
|
1043
|
+
operation: input.operation,
|
|
1044
|
+
context: input.context,
|
|
1045
|
+
bindingId: input.bindingId,
|
|
1046
|
+
stableId: input.stableId,
|
|
1047
|
+
provenance: input.provenance,
|
|
1048
|
+
fieldMask: input.fieldMask,
|
|
1049
|
+
projection: input.projection,
|
|
1050
|
+
postconditions: input.projection,
|
|
1051
|
+
executionEvidence: input.executionEvidence,
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
function linearMutationActionDigest(input) {
|
|
1055
|
+
return semanticDigest({
|
|
1056
|
+
provider: 'linear',
|
|
1057
|
+
operation: input.operation,
|
|
1058
|
+
context: input.context,
|
|
1059
|
+
bindingId: input.bindingId,
|
|
1060
|
+
stableId: input.stableId,
|
|
1061
|
+
provenance: input.provenance,
|
|
1062
|
+
fieldMask: input.fieldMask,
|
|
1063
|
+
projection: input.projection,
|
|
1064
|
+
previewDigest: input.previewDigest,
|
|
1065
|
+
approvalDigest: input.approvalDigest,
|
|
1066
|
+
executionEvidence: input.executionEvidence,
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
function validLinearReadAction(action) {
|
|
1070
|
+
if (action.provider !== 'linear' ||
|
|
1071
|
+
action.operation !== 'read' ||
|
|
1072
|
+
!hasPinnedLinearContext(action.context)) {
|
|
1073
|
+
return false;
|
|
1074
|
+
}
|
|
1075
|
+
const intent = action.intent;
|
|
1076
|
+
if (typeof intent.uuid !== 'string' ||
|
|
1077
|
+
!UUID.test(intent.uuid) ||
|
|
1078
|
+
typeof intent.stableId !== 'string' ||
|
|
1079
|
+
intent.stableId !==
|
|
1080
|
+
canonicalLinearStableId(action.context.workspaceId, intent.uuid) ||
|
|
1081
|
+
(intent.currentIdentifier !== null &&
|
|
1082
|
+
(typeof intent.currentIdentifier !== 'string' ||
|
|
1083
|
+
!IDENTIFIER.test(intent.currentIdentifier))) ||
|
|
1084
|
+
typeof intent.stepId !== 'string' ||
|
|
1085
|
+
intent.stepId.length === 0 ||
|
|
1086
|
+
Buffer.byteLength(intent.stepId, 'utf8') > 128 ||
|
|
1087
|
+
typeof intent.capabilityEvidenceDigest !== 'string' ||
|
|
1088
|
+
intent.capabilityEvidenceDigest.length === 0) {
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
const resultContract = {
|
|
1092
|
+
requireStableIdentity: true,
|
|
1093
|
+
requireExactContext: true,
|
|
1094
|
+
allowedFields: [
|
|
1095
|
+
'uuid',
|
|
1096
|
+
'identifier',
|
|
1097
|
+
'historicalIdentifiers',
|
|
1098
|
+
'workspaceId',
|
|
1099
|
+
'teamId',
|
|
1100
|
+
'historicalTeamIds',
|
|
1101
|
+
'title',
|
|
1102
|
+
'description',
|
|
1103
|
+
'state',
|
|
1104
|
+
'priority',
|
|
1105
|
+
'archived',
|
|
1106
|
+
...LINEAR_EXTENSION_KEYS,
|
|
1107
|
+
],
|
|
1108
|
+
};
|
|
1109
|
+
const readIntent = {
|
|
1110
|
+
stableId: intent.stableId,
|
|
1111
|
+
uuid: intent.uuid,
|
|
1112
|
+
currentIdentifier: intent.currentIdentifier,
|
|
1113
|
+
stepId: intent.stepId,
|
|
1114
|
+
capabilityEvidenceDigest: intent.capabilityEvidenceDigest,
|
|
1115
|
+
resultContract,
|
|
1116
|
+
};
|
|
1117
|
+
const actionDigest = semanticDigest({
|
|
1118
|
+
provider: 'linear',
|
|
1119
|
+
operation: 'read',
|
|
1120
|
+
context: action.context,
|
|
1121
|
+
intent: readIntent,
|
|
1122
|
+
});
|
|
1123
|
+
return semanticValuesEqual(action.intent, { ...readIntent, actionDigest });
|
|
1124
|
+
}
|
|
1125
|
+
function validLinearDiscussionAction(action) {
|
|
1126
|
+
if (action.provider !== 'linear' ||
|
|
1127
|
+
action.operation !== 'read-discussion' ||
|
|
1128
|
+
!hasPinnedLinearContext(action.context) ||
|
|
1129
|
+
typeof action.intent.stableId !== 'string' ||
|
|
1130
|
+
!validLinearStableIdForContext(action.intent.stableId, action.context) ||
|
|
1131
|
+
!Number.isInteger(action.intent.limit) ||
|
|
1132
|
+
Number(action.intent.limit) < 1 ||
|
|
1133
|
+
Number(action.intent.limit) > LINEAR_DISCUSSION_LIMITS.maxItems ||
|
|
1134
|
+
(action.intent.cursor !== null &&
|
|
1135
|
+
(typeof action.intent.cursor !== 'string' ||
|
|
1136
|
+
Buffer.byteLength(action.intent.cursor, 'utf8') >
|
|
1137
|
+
LINEAR_DISCUSSION_LIMITS.maxCursorBytes)) ||
|
|
1138
|
+
typeof action.intent.capabilityEvidenceDigest !== 'string' ||
|
|
1139
|
+
action.intent.capabilityEvidenceDigest.length === 0) {
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
const intent = {
|
|
1143
|
+
stableId: action.intent.stableId,
|
|
1144
|
+
cursor: action.intent.cursor,
|
|
1145
|
+
limit: action.intent.limit,
|
|
1146
|
+
resultContract: {
|
|
1147
|
+
maxItems: action.intent.limit,
|
|
1148
|
+
maxIdBytes: LINEAR_DISCUSSION_LIMITS.maxIdBytes,
|
|
1149
|
+
maxBodyBytes: LINEAR_DISCUSSION_LIMITS.maxBodyBytes,
|
|
1150
|
+
maxCursorBytes: LINEAR_DISCUSSION_LIMITS.maxCursorBytes,
|
|
1151
|
+
maxPageBytes: LINEAR_DISCUSSION_LIMITS.maxPageBytes,
|
|
1152
|
+
persistable: false,
|
|
1153
|
+
contentPolicy: 'sanitized-whole-field-suppression',
|
|
1154
|
+
},
|
|
1155
|
+
capabilityEvidenceDigest: action.intent.capabilityEvidenceDigest,
|
|
1156
|
+
};
|
|
1157
|
+
const actionDigest = semanticDigest({
|
|
1158
|
+
provider: 'linear',
|
|
1159
|
+
operation: 'read-discussion',
|
|
1160
|
+
context: action.context,
|
|
1161
|
+
intent,
|
|
1162
|
+
});
|
|
1163
|
+
return semanticValuesEqual(action.intent, { ...intent, actionDigest });
|
|
1164
|
+
}
|
|
1165
|
+
function validLinearDuplicateAction(action) {
|
|
1166
|
+
if (action.provider !== 'linear' ||
|
|
1167
|
+
action.operation !== 'search-duplicates' ||
|
|
1168
|
+
!hasPinnedLinearContext(action.context) ||
|
|
1169
|
+
!isRecord(action.intent.query) ||
|
|
1170
|
+
!isRecord(action.intent.resultContract) ||
|
|
1171
|
+
typeof action.intent.capabilityEvidenceDigest !== 'string' ||
|
|
1172
|
+
action.intent.capabilityEvidenceDigest.length === 0) {
|
|
1173
|
+
return false;
|
|
1174
|
+
}
|
|
1175
|
+
const query = action.intent.query;
|
|
1176
|
+
const identifiers = query.historicalIdentifiers;
|
|
1177
|
+
const maxResults = Number(action.intent.resultContract.maxResults);
|
|
1178
|
+
if (typeof query.provenanceToken !== 'string' ||
|
|
1179
|
+
query.provenanceToken.length === 0 ||
|
|
1180
|
+
query.provenanceToken.length > 512 ||
|
|
1181
|
+
typeof query.reservedBindingId !== 'string' ||
|
|
1182
|
+
query.reservedBindingId.length === 0 ||
|
|
1183
|
+
query.reservedBindingId.length > 128 ||
|
|
1184
|
+
!Array.isArray(identifiers) ||
|
|
1185
|
+
identifiers.some((identifier) => typeof identifier !== 'string' || !IDENTIFIER.test(identifier)) ||
|
|
1186
|
+
identifiers.length > 64 ||
|
|
1187
|
+
new Set(identifiers).size !== identifiers.length ||
|
|
1188
|
+
query.workspaceId !== action.context.workspaceId ||
|
|
1189
|
+
query.teamId !== action.context.teamId ||
|
|
1190
|
+
!Number.isInteger(maxResults) ||
|
|
1191
|
+
maxResults < 1 ||
|
|
1192
|
+
maxResults > 100) {
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
const expectedQuery = {
|
|
1196
|
+
provenanceToken: query.provenanceToken,
|
|
1197
|
+
reservedBindingId: query.reservedBindingId,
|
|
1198
|
+
historicalIdentifiers: identifiers,
|
|
1199
|
+
workspaceId: action.context.workspaceId,
|
|
1200
|
+
teamId: action.context.teamId,
|
|
1201
|
+
};
|
|
1202
|
+
const expectedContract = {
|
|
1203
|
+
maxResults,
|
|
1204
|
+
classifications: ['no-match', 'one-match', 'ambiguous'],
|
|
1205
|
+
requireStableUuid: true,
|
|
1206
|
+
requireExactContext: true,
|
|
1207
|
+
matchStatus: 'evidence-until-identity-and-context-verified',
|
|
1208
|
+
};
|
|
1209
|
+
return (semanticValuesEqual(query, expectedQuery) &&
|
|
1210
|
+
action.intent.queryDigest === semanticDigest(expectedQuery) &&
|
|
1211
|
+
semanticValuesEqual(action.intent.resultContract, expectedContract));
|
|
1212
|
+
}
|
|
1213
|
+
function validLinearStableIdForContext(stableId, context) {
|
|
1214
|
+
if (typeof stableId !== 'string' || typeof context.workspaceId !== 'string') {
|
|
1215
|
+
return false;
|
|
1216
|
+
}
|
|
1217
|
+
const prefix = `linear:${context.workspaceId}:`;
|
|
1218
|
+
return (stableId.startsWith(prefix) && UUID.test(stableId.slice(prefix.length)));
|
|
1219
|
+
}
|
|
1220
|
+
function isLinearSpecializedOperation(operation) {
|
|
1221
|
+
return operation === 'read-discussion' || operation === 'search-duplicates';
|
|
1222
|
+
}
|
|
1223
|
+
function semanticValuesEqual(left, right) {
|
|
1224
|
+
try {
|
|
1225
|
+
return semanticDigest(left) === semanticDigest(right);
|
|
1226
|
+
}
|
|
1227
|
+
catch {
|
|
1228
|
+
return false;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
function isRecord(value) {
|
|
1232
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1233
|
+
}
|
|
1234
|
+
function parseLinearHostCapability(value) {
|
|
1235
|
+
if (!isRecord(value) ||
|
|
1236
|
+
value.provider !== 'linear' ||
|
|
1237
|
+
!isRecord(value.context) ||
|
|
1238
|
+
!Array.isArray(value.operations) ||
|
|
1239
|
+
!Array.isArray(value.observableFields) ||
|
|
1240
|
+
typeof value.availability !== 'string' ||
|
|
1241
|
+
typeof value.accountId !== 'string' ||
|
|
1242
|
+
typeof value.workspaceId !== 'string' ||
|
|
1243
|
+
typeof value.teamId !== 'string' ||
|
|
1244
|
+
typeof value.evidenceDigest !== 'string') {
|
|
1245
|
+
return null;
|
|
1246
|
+
}
|
|
1247
|
+
return value;
|
|
1248
|
+
}
|
|
1249
|
+
function isCompleteLinearMutationInput(input, operation) {
|
|
1250
|
+
return (input.operation === operation &&
|
|
1251
|
+
isRecord(input.context) &&
|
|
1252
|
+
isRecord(input.hostCapability) &&
|
|
1253
|
+
typeof input.bindingId === 'string' &&
|
|
1254
|
+
Array.isArray(input.fieldMask) &&
|
|
1255
|
+
isRecord(input.projection) &&
|
|
1256
|
+
isRecord(input.outboundSafety) &&
|
|
1257
|
+
typeof input.approvedPreviewDigest === 'string');
|
|
1258
|
+
}
|
|
1259
|
+
function isCompleteLinearReadInput(input) {
|
|
1260
|
+
return (isRecord(input.context) &&
|
|
1261
|
+
isRecord(input.hostCapability) &&
|
|
1262
|
+
typeof input.stableId === 'string' &&
|
|
1263
|
+
typeof input.uuid === 'string' &&
|
|
1264
|
+
typeof input.stepId === 'string');
|
|
1265
|
+
}
|
|
1266
|
+
function isCompleteLinearDiscussionInput(input) {
|
|
1267
|
+
return (isRecord(input.context) &&
|
|
1268
|
+
isRecord(input.hostCapability) &&
|
|
1269
|
+
typeof input.stableId === 'string' &&
|
|
1270
|
+
Number.isInteger(input.limit) &&
|
|
1271
|
+
(input.cursor === null || typeof input.cursor === 'string'));
|
|
1272
|
+
}
|
|
1273
|
+
function isCompleteLinearDuplicateSearchInput(input) {
|
|
1274
|
+
return (isRecord(input.context) &&
|
|
1275
|
+
isRecord(input.hostCapability) &&
|
|
1276
|
+
typeof input.provenanceToken === 'string' &&
|
|
1277
|
+
typeof input.reservedBindingId === 'string' &&
|
|
1278
|
+
Array.isArray(input.historicalIdentifiers) &&
|
|
1279
|
+
Number.isInteger(input.maxResults));
|
|
1280
|
+
}
|
|
1281
|
+
function validLinearDuplicateCandidateBounds(candidate) {
|
|
1282
|
+
return (UUID.test(candidate.uuid) &&
|
|
1283
|
+
candidate.stableId.length > 0 &&
|
|
1284
|
+
candidate.stableId.length <= 512 &&
|
|
1285
|
+
candidate.identifiers.length <= 64 &&
|
|
1286
|
+
new Set(candidate.identifiers).size === candidate.identifiers.length &&
|
|
1287
|
+
candidate.identifiers.every((identifier) => IDENTIFIER.test(identifier)) &&
|
|
1288
|
+
['provenance', 'reserved-binding', 'identifier'].includes(candidate.matchedBy));
|
|
1289
|
+
}
|