@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.
Files changed (142) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/cli-utilities/configuration.md +19 -1
  3. package/assets/docs/cli-utilities/index.md +3 -0
  4. package/assets/docs/cli-utilities/remote-project-management.md +191 -0
  5. package/assets/docs/reference/cli-reference.md +12 -1
  6. package/assets/docs/reference/file-locations.md +22 -0
  7. package/assets/docs/reference/oat-directory-structure.md +30 -17
  8. package/assets/docs/reference/troubleshooting.md +44 -0
  9. package/assets/docs/workflows/skills/index.md +33 -0
  10. package/assets/public-package-versions.json +4 -4
  11. package/assets/skills/oat-doctor/SKILL.md +4 -4
  12. package/assets/skills/oat-pjm-remote/SKILL.md +101 -0
  13. package/assets/skills/oat-pjm-remote/references/external-action-protocol.md +43 -0
  14. package/dist/commands/backlog/new.d.ts +1 -0
  15. package/dist/commands/backlog/new.d.ts.map +1 -1
  16. package/dist/commands/backlog/new.js +3 -1
  17. package/dist/commands/config/index.d.ts +1 -0
  18. package/dist/commands/config/index.d.ts.map +1 -1
  19. package/dist/commands/config/index.js +232 -1
  20. package/dist/commands/pjm/doctor.d.ts +2 -0
  21. package/dist/commands/pjm/doctor.d.ts.map +1 -1
  22. package/dist/commands/pjm/doctor.js +71 -1
  23. package/dist/commands/pjm/index.d.ts +2 -0
  24. package/dist/commands/pjm/index.d.ts.map +1 -1
  25. package/dist/commands/pjm/index.js +3 -0
  26. package/dist/commands/pjm/remote/__integration__/lifecycle-harness.d.ts +60 -0
  27. package/dist/commands/pjm/remote/__integration__/lifecycle-harness.d.ts.map +1 -0
  28. package/dist/commands/pjm/remote/__integration__/lifecycle-harness.js +151 -0
  29. package/dist/commands/pjm/remote/association.d.ts +27 -0
  30. package/dist/commands/pjm/remote/association.d.ts.map +1 -0
  31. package/dist/commands/pjm/remote/association.js +68 -0
  32. package/dist/commands/pjm/remote/authority.d.ts +210 -0
  33. package/dist/commands/pjm/remote/authority.d.ts.map +1 -0
  34. package/dist/commands/pjm/remote/authority.js +341 -0
  35. package/dist/commands/pjm/remote/batch.d.ts +15 -0
  36. package/dist/commands/pjm/remote/batch.d.ts.map +1 -0
  37. package/dist/commands/pjm/remote/batch.js +126 -0
  38. package/dist/commands/pjm/remote/closeout.d.ts +69 -0
  39. package/dist/commands/pjm/remote/closeout.d.ts.map +1 -0
  40. package/dist/commands/pjm/remote/closeout.js +208 -0
  41. package/dist/commands/pjm/remote/create-binding.d.ts +62 -0
  42. package/dist/commands/pjm/remote/create-binding.d.ts.map +1 -0
  43. package/dist/commands/pjm/remote/create-binding.js +126 -0
  44. package/dist/commands/pjm/remote/credential-safety.d.ts +7 -0
  45. package/dist/commands/pjm/remote/credential-safety.d.ts.map +1 -0
  46. package/dist/commands/pjm/remote/credential-safety.js +21 -0
  47. package/dist/commands/pjm/remote/discussion.d.ts +49 -0
  48. package/dist/commands/pjm/remote/discussion.d.ts.map +1 -0
  49. package/dist/commands/pjm/remote/discussion.js +124 -0
  50. package/dist/commands/pjm/remote/doctor.d.ts +22 -0
  51. package/dist/commands/pjm/remote/doctor.d.ts.map +1 -0
  52. package/dist/commands/pjm/remote/doctor.js +380 -0
  53. package/dist/commands/pjm/remote/external-action.d.ts +208 -0
  54. package/dist/commands/pjm/remote/external-action.d.ts.map +1 -0
  55. package/dist/commands/pjm/remote/external-action.js +367 -0
  56. package/dist/commands/pjm/remote/host-execution.d.ts +28 -0
  57. package/dist/commands/pjm/remote/host-execution.d.ts.map +1 -0
  58. package/dist/commands/pjm/remote/host-execution.js +63 -0
  59. package/dist/commands/pjm/remote/index.d.ts +41 -0
  60. package/dist/commands/pjm/remote/index.d.ts.map +1 -0
  61. package/dist/commands/pjm/remote/index.js +280 -0
  62. package/dist/commands/pjm/remote/jira-adf.d.ts +35 -0
  63. package/dist/commands/pjm/remote/jira-adf.d.ts.map +1 -0
  64. package/dist/commands/pjm/remote/jira-adf.js +169 -0
  65. package/dist/commands/pjm/remote/lifecycle.d.ts +163 -0
  66. package/dist/commands/pjm/remote/lifecycle.d.ts.map +1 -0
  67. package/dist/commands/pjm/remote/lifecycle.js +224 -0
  68. package/dist/commands/pjm/remote/local-projection.d.ts +30 -0
  69. package/dist/commands/pjm/remote/local-projection.d.ts.map +1 -0
  70. package/dist/commands/pjm/remote/local-projection.js +95 -0
  71. package/dist/commands/pjm/remote/managed-markdown.d.ts +30 -0
  72. package/dist/commands/pjm/remote/managed-markdown.d.ts.map +1 -0
  73. package/dist/commands/pjm/remote/managed-markdown.js +128 -0
  74. package/dist/commands/pjm/remote/migrate.d.ts +14 -0
  75. package/dist/commands/pjm/remote/migrate.d.ts.map +1 -0
  76. package/dist/commands/pjm/remote/migrate.js +32 -0
  77. package/dist/commands/pjm/remote/operation-state.d.ts +15 -0
  78. package/dist/commands/pjm/remote/operation-state.d.ts.map +1 -0
  79. package/dist/commands/pjm/remote/operation-state.js +143 -0
  80. package/dist/commands/pjm/remote/outbound-projection-safety.d.ts +25 -0
  81. package/dist/commands/pjm/remote/outbound-projection-safety.d.ts.map +1 -0
  82. package/dist/commands/pjm/remote/outbound-projection-safety.js +99 -0
  83. package/dist/commands/pjm/remote/output.d.ts +60 -0
  84. package/dist/commands/pjm/remote/output.d.ts.map +1 -0
  85. package/dist/commands/pjm/remote/output.js +48 -0
  86. package/dist/commands/pjm/remote/preview.d.ts +88 -0
  87. package/dist/commands/pjm/remote/preview.d.ts.map +1 -0
  88. package/dist/commands/pjm/remote/preview.js +154 -0
  89. package/dist/commands/pjm/remote/provider-conformance.d.ts +13 -0
  90. package/dist/commands/pjm/remote/provider-conformance.d.ts.map +1 -0
  91. package/dist/commands/pjm/remote/provider-conformance.js +37 -0
  92. package/dist/commands/pjm/remote/provider.d.ts +57 -0
  93. package/dist/commands/pjm/remote/provider.d.ts.map +1 -0
  94. package/dist/commands/pjm/remote/provider.js +28 -0
  95. package/dist/commands/pjm/remote/providers/github-publication-safety.d.ts +46 -0
  96. package/dist/commands/pjm/remote/providers/github-publication-safety.d.ts.map +1 -0
  97. package/dist/commands/pjm/remote/providers/github-publication-safety.js +126 -0
  98. package/dist/commands/pjm/remote/providers/github.d.ts +207 -0
  99. package/dist/commands/pjm/remote/providers/github.d.ts.map +1 -0
  100. package/dist/commands/pjm/remote/providers/github.js +1456 -0
  101. package/dist/commands/pjm/remote/providers/jira.d.ts +245 -0
  102. package/dist/commands/pjm/remote/providers/jira.d.ts.map +1 -0
  103. package/dist/commands/pjm/remote/providers/jira.js +1650 -0
  104. package/dist/commands/pjm/remote/providers/linear.d.ts +182 -0
  105. package/dist/commands/pjm/remote/providers/linear.d.ts.map +1 -0
  106. package/dist/commands/pjm/remote/providers/linear.js +1289 -0
  107. package/dist/commands/pjm/remote/purpose-policy.d.ts +29 -0
  108. package/dist/commands/pjm/remote/purpose-policy.d.ts.map +1 -0
  109. package/dist/commands/pjm/remote/purpose-policy.js +118 -0
  110. package/dist/commands/pjm/remote/reconcile.d.ts +42 -0
  111. package/dist/commands/pjm/remote/reconcile.d.ts.map +1 -0
  112. package/dist/commands/pjm/remote/reconcile.js +101 -0
  113. package/dist/commands/pjm/remote/resolution.d.ts +122 -0
  114. package/dist/commands/pjm/remote/resolution.d.ts.map +1 -0
  115. package/dist/commands/pjm/remote/resolution.js +243 -0
  116. package/dist/commands/pjm/remote/schema.d.ts +9309 -0
  117. package/dist/commands/pjm/remote/schema.d.ts.map +1 -0
  118. package/dist/commands/pjm/remote/schema.js +1503 -0
  119. package/dist/commands/pjm/remote/service.d.ts +20 -0
  120. package/dist/commands/pjm/remote/service.d.ts.map +1 -0
  121. package/dist/commands/pjm/remote/service.js +5121 -0
  122. package/dist/commands/pjm/remote/shared-storage.d.ts +36 -0
  123. package/dist/commands/pjm/remote/shared-storage.d.ts.map +1 -0
  124. package/dist/commands/pjm/remote/shared-storage.js +110 -0
  125. package/dist/commands/pjm/remote/snapshot.d.ts +32 -0
  126. package/dist/commands/pjm/remote/snapshot.d.ts.map +1 -0
  127. package/dist/commands/pjm/remote/snapshot.js +140 -0
  128. package/dist/commands/pjm/remote/storage-locator.d.ts +29 -0
  129. package/dist/commands/pjm/remote/storage-locator.d.ts.map +1 -0
  130. package/dist/commands/pjm/remote/storage-locator.js +62 -0
  131. package/dist/commands/pjm/remote/store.d.ts +85 -0
  132. package/dist/commands/pjm/remote/store.d.ts.map +1 -0
  133. package/dist/commands/pjm/remote/store.js +553 -0
  134. package/dist/commands/pjm/remote/verification.d.ts +40 -0
  135. package/dist/commands/pjm/remote/verification.d.ts.map +1 -0
  136. package/dist/commands/pjm/remote/verification.js +100 -0
  137. package/dist/commands/tools/shared/pack-manifest.d.ts.map +1 -1
  138. package/dist/commands/tools/shared/pack-manifest.js +1 -0
  139. package/dist/config/oat-config.d.ts +26 -0
  140. package/dist/config/oat-config.d.ts.map +1 -1
  141. package/dist/config/oat-config.js +186 -2
  142. package/package.json +2 -2
@@ -0,0 +1,1650 @@
1
+ import { containsSensitiveContentSignal } from '../../../pjm/remote/credential-safety.js';
2
+ import { inspectJiraAdf, validateJiraAdfDocument, verifyJiraAdfReplacement, } from '../../../pjm/remote/jira-adf.js';
3
+ import { requireCurrentOutboundSafety, } from '../../../pjm/remote/outbound-projection-safety.js';
4
+ import { contextsEqual, semanticDigest, } from '../../../pjm/remote/provider.js';
5
+ import { WHOLE_FIELD_SUPPRESSION_MARKER } from '../../../pjm/remote/schema.js';
6
+ const ISSUE_KEY = /^[A-Z][A-Z0-9_]*-[1-9][0-9]*$/;
7
+ const ISSUE_ID = /^[1-9][0-9]*$/;
8
+ const URL_REFERENCE = /^https:\/\/([^/]+)\/browse\/([A-Z][A-Z0-9_]*-[1-9][0-9]*)(?:[?#].*)?$/;
9
+ const JIRA_EXTENSION_KEYS = ['issueType', 'priorityId', 'statusId'];
10
+ export const JIRA_DISCUSSION_LIMITS = {
11
+ maxItems: 100,
12
+ maxIdBytes: 256,
13
+ maxBodyBytes: 16_384,
14
+ maxCursorBytes: 512,
15
+ maxPageBytes: 65_536,
16
+ };
17
+ export function parseJiraIssueReference(reference) {
18
+ const value = reference.trim();
19
+ const url = URL_REFERENCE.exec(value);
20
+ if (url)
21
+ return { host: url[1].toLowerCase(), key: url[2], alias: value };
22
+ if (!ISSUE_KEY.test(value))
23
+ return null;
24
+ return { key: value, alias: value };
25
+ }
26
+ export function normalizeJiraIssueObservation(observation) {
27
+ assertJiraObservation(observation);
28
+ const siteId = requiredContext(observation, 'siteId');
29
+ const projectId = requiredContext(observation, 'projectId');
30
+ const issueId = requiredString(observation.fields, 'issueId');
31
+ if (!ISSUE_ID.test(issueId))
32
+ throw new Error('Jira issue ID is invalid.');
33
+ const stableId = canonicalJiraStableId(siteId, issueId);
34
+ if (observation.identity.stableId !== issueId &&
35
+ observation.identity.stableId !== stableId) {
36
+ throw new Error('Jira observation stable identity does not match issue ID.');
37
+ }
38
+ if (observation.fields.siteId !== siteId ||
39
+ observation.fields.projectId !== projectId) {
40
+ throw new Error('Jira observation context does not match.');
41
+ }
42
+ const key = requiredString(observation.fields, 'key');
43
+ if (!ISSUE_KEY.test(key))
44
+ throw new Error('Jira issue key is invalid.');
45
+ const historicalKeys = stringArray(observation.fields.historicalKeys).filter((item) => item !== key);
46
+ if (historicalKeys.some((item) => !ISSUE_KEY.test(item))) {
47
+ throw new Error('Jira historical issue key is invalid.');
48
+ }
49
+ const historicalProjectIds = stringArray(observation.fields.historicalProjectIds).filter((item) => item !== projectId);
50
+ const suppressedFields = [];
51
+ const title = suppressInboundText(requiredString(observation.fields, 'title'), 'title', suppressedFields);
52
+ const description = suppressNullableInboundText(observation.fields.descriptionText, 'description', suppressedFields);
53
+ const descriptionAdf = observation.fields.descriptionAdf ?? null;
54
+ if (descriptionAdf !== null)
55
+ validateJiraAdfDocument(descriptionAdf);
56
+ const extensions = {
57
+ issueId,
58
+ siteId,
59
+ projectId,
60
+ currentKey: key,
61
+ historicalKeys,
62
+ historicalProjectIds,
63
+ descriptionAdf,
64
+ lifecycle: nullableString(observation.fields.lifecycle),
65
+ capabilityEvidenceDigest: observation.capabilityEvidenceDigest,
66
+ suppressedFields,
67
+ };
68
+ const validatedCapabilityEvidence = normalizedJiraCapabilityEvidence(observation);
69
+ if (validatedCapabilityEvidence) {
70
+ extensions.validatedCapabilityEvidence = validatedCapabilityEvidence;
71
+ }
72
+ for (const extension of JIRA_EXTENSION_KEYS) {
73
+ const value = observation.fields[extension];
74
+ if (value === null || typeof value === 'string')
75
+ extensions[extension] = value;
76
+ }
77
+ if (isRecord(observation.fields.mutationEvidence)) {
78
+ extensions.mutationEvidence = observation.fields.mutationEvidence;
79
+ }
80
+ if (isRecord(observation.fields.createProvenance)) {
81
+ extensions.createProvenance = observation.fields.createProvenance;
82
+ }
83
+ if (Array.isArray(observation.fields.annotations)) {
84
+ extensions.annotations = stringArray(observation.fields.annotations);
85
+ }
86
+ return {
87
+ provider: 'jira',
88
+ context: observation.context,
89
+ stableId,
90
+ aliases: uniqueStrings([
91
+ key,
92
+ ...historicalKeys,
93
+ ...observation.identity.aliases,
94
+ ]),
95
+ title,
96
+ description,
97
+ priority: nullableString(observation.fields.priority),
98
+ status: requiredString(observation.fields, 'status'),
99
+ revisionDigest: semanticDigest(observation.revision),
100
+ extensions,
101
+ };
102
+ }
103
+ export function validateJiraHostCapability(operation, context, observed, requiredFields = []) {
104
+ const reasons = [];
105
+ if (observed.provider !== 'jira')
106
+ reasons.push('provider-mismatch');
107
+ if (observed.availability !== 'available')
108
+ reasons.push(observed.availability);
109
+ if (!hasPinnedJiraContext(context) ||
110
+ !contextsEqual(context, observed.context)) {
111
+ reasons.push('context-mismatch');
112
+ }
113
+ if (context.accountId !== observed.accountId)
114
+ reasons.push('account-mismatch');
115
+ if (context.siteId !== observed.siteId)
116
+ reasons.push('site-mismatch');
117
+ if (context.projectId !== observed.projectId)
118
+ reasons.push('project-mismatch');
119
+ if (!observed.operations.includes(operation))
120
+ reasons.push(`capability-missing:${operation}`);
121
+ for (const field of requiredFields) {
122
+ if (!observed.observableFields.includes(field))
123
+ reasons.push(`semantic-field-missing:${field}`);
124
+ }
125
+ if (!observed.evidenceDigest)
126
+ reasons.push('capability-evidence-missing');
127
+ if (!Array.isArray(observed.semanticCapabilities) ||
128
+ observed.semanticCapabilities.length > 1 ||
129
+ new Set(observed.semanticCapabilities).size !==
130
+ observed.semanticCapabilities.length ||
131
+ observed.semanticCapabilities.some((capability) => capability !== 'structural-description-write')) {
132
+ reasons.push('semantic-capability-evidence-invalid');
133
+ }
134
+ if (!Number.isFinite(Date.parse(observed.observedAt))) {
135
+ reasons.push('capability-freshness-invalid');
136
+ }
137
+ return { valid: reasons.length === 0, reasons };
138
+ }
139
+ export function planJiraRead(input) {
140
+ if (!hasPinnedJiraContext(input.context) ||
141
+ !ISSUE_ID.test(input.issueId) ||
142
+ input.stableId !==
143
+ canonicalJiraStableId(input.context.siteId, input.issueId) ||
144
+ (input.currentKey !== null && !ISSUE_KEY.test(input.currentKey)) ||
145
+ !input.stepId ||
146
+ Buffer.byteLength(input.stepId, 'utf8') > 128) {
147
+ throw new Error('Jira read plan requires pinned identity and context.');
148
+ }
149
+ requireJiraCapability(input.hostCapability, input.context, 'read', [
150
+ 'stable-identity',
151
+ 'title',
152
+ 'status',
153
+ 'revision',
154
+ 'project-context',
155
+ ]);
156
+ return jiraAction('read', input.context, {
157
+ kind: 'issue',
158
+ stableId: input.stableId,
159
+ issueId: input.issueId,
160
+ currentKey: input.currentKey,
161
+ stepId: input.stepId,
162
+ capabilityEvidenceDigest: input.hostCapability.evidenceDigest,
163
+ capabilityObservedAt: input.hostCapability.observedAt,
164
+ resultContract: jiraIssueResultContract(),
165
+ });
166
+ }
167
+ export function planJiraMetadataRead(input) {
168
+ requireJiraCapability(input.hostCapability, input.context, 'read', [
169
+ 'metadata',
170
+ ...(input.purpose === 'transition'
171
+ ? ['transitions']
172
+ : []),
173
+ ]);
174
+ if (input.issueId !== undefined && !ISSUE_ID.test(input.issueId)) {
175
+ throw new Error('Jira metadata read issue identity is invalid.');
176
+ }
177
+ return jiraAction('read', input.context, {
178
+ kind: 'metadata',
179
+ purpose: input.purpose,
180
+ issueId: input.issueId ?? null,
181
+ capabilityEvidenceDigest: input.hostCapability.evidenceDigest,
182
+ capabilityObservedAt: input.hostCapability.observedAt,
183
+ resultContract: jiraMetadataResultContract(input.purpose),
184
+ });
185
+ }
186
+ export function planJiraDiscussionRead(input) {
187
+ requireJiraCapability(input.hostCapability, input.context, 'read-discussion');
188
+ if (!validJiraStableIdForContext(input.stableId, input.context) ||
189
+ !Number.isInteger(input.limit) ||
190
+ input.limit < 1 ||
191
+ input.limit > JIRA_DISCUSSION_LIMITS.maxItems ||
192
+ (input.cursor !== null &&
193
+ Buffer.byteLength(input.cursor, 'utf8') >
194
+ JIRA_DISCUSSION_LIMITS.maxCursorBytes)) {
195
+ throw new Error('Jira discussion read bounds are invalid.');
196
+ }
197
+ return jiraAction('read-discussion', input.context, {
198
+ stableId: input.stableId,
199
+ cursor: input.cursor,
200
+ limit: input.limit,
201
+ capabilityEvidenceDigest: input.hostCapability.evidenceDigest,
202
+ capabilityObservedAt: input.hostCapability.observedAt,
203
+ resultContract: jiraDiscussionResultContract(input.limit),
204
+ });
205
+ }
206
+ export function previewJiraMutation(input) {
207
+ requireJiraCapability(input.hostCapability, input.context, input.operation);
208
+ assertJiraMutationIdentity(input);
209
+ const fieldMask = normalizeJiraMutationFieldMask(input);
210
+ if (fieldMask.includes('description')) {
211
+ requireJiraSemanticCapability(input.hostCapability, 'structural-description-write');
212
+ }
213
+ requireCurrentOutboundSafety(input.projection, input.outboundSafety);
214
+ assertJiraMetadataSupports(input, fieldMask);
215
+ const metadataContract = normalizeJiraMetadataContract(input.normalizedMetadata);
216
+ const requiredSemanticCapabilities = fieldMask.includes('description')
217
+ ? ['structural-description-write']
218
+ : [];
219
+ const descriptionAdfEvidence = buildJiraDescriptionAdfEvidence(input, fieldMask);
220
+ const structuralDescriptionWrite = buildJiraStructuralDescriptionWrite(input, descriptionAdfEvidence);
221
+ const executionEvidence = {
222
+ capabilityEvidenceDigest: input.hostCapability.evidenceDigest,
223
+ capabilityObservedAt: input.hostCapability.observedAt,
224
+ metadataEvidenceDigest: input.normalizedMetadata.evidenceDigest,
225
+ metadataSemanticsDigest: semanticDigest(metadataContract),
226
+ projectionDigest: input.outboundSafety.projectionDigest,
227
+ outboundSafetyResultDigest: input.outboundSafety.resultDigest,
228
+ descriptionAdfEvidence,
229
+ };
230
+ return {
231
+ previewDigest: semanticDigest({
232
+ provider: 'jira',
233
+ operation: input.operation,
234
+ context: input.context,
235
+ bindingId: input.bindingId,
236
+ stableId: input.stableId ?? null,
237
+ provenance: input.provenance ?? null,
238
+ fieldMask,
239
+ projection: input.projection,
240
+ postconditions: input.projection,
241
+ metadataContract,
242
+ structuralDescriptionWrite,
243
+ requiredSemanticCapabilities,
244
+ executionEvidence,
245
+ }),
246
+ metadataContract,
247
+ structuralDescriptionWrite,
248
+ requiredSemanticCapabilities: [...requiredSemanticCapabilities],
249
+ executionEvidence,
250
+ };
251
+ }
252
+ export function planJiraMutation(input) {
253
+ const preview = previewJiraMutation(input);
254
+ if (input.approvedPreviewDigest !== preview.previewDigest) {
255
+ throw new Error('Jira mutation approval does not match its preview.');
256
+ }
257
+ const fieldMask = normalizeJiraMutationFieldMask(input);
258
+ const baseIntent = {
259
+ bindingId: input.bindingId,
260
+ stableId: input.stableId ?? null,
261
+ provenance: input.provenance ?? null,
262
+ fieldMask,
263
+ projection: { ...input.projection },
264
+ postconditions: { ...input.projection },
265
+ capabilityEvidenceDigest: input.hostCapability.evidenceDigest,
266
+ metadataEvidenceDigest: input.normalizedMetadata.evidenceDigest,
267
+ metadataSemanticsDigest: preview.executionEvidence.metadataSemanticsDigest,
268
+ metadataContract: preview.metadataContract,
269
+ structuralDescriptionWrite: preview.structuralDescriptionWrite,
270
+ requiredSemanticCapabilities: preview.requiredSemanticCapabilities,
271
+ outboundSafety: {
272
+ projectionDigest: input.outboundSafety.projectionDigest,
273
+ resultDigest: input.outboundSafety.resultDigest,
274
+ },
275
+ outboundSafetyEvidence: { ...input.outboundSafety },
276
+ previewDigest: preview.previewDigest,
277
+ approvalDigest: input.approvedPreviewDigest,
278
+ readbackContract: {
279
+ pinned: true,
280
+ requireStableIdentity: true,
281
+ requireExactContext: true,
282
+ fields: fieldMask,
283
+ },
284
+ };
285
+ const actionDigest = semanticDigest({
286
+ provider: 'jira',
287
+ operation: input.operation,
288
+ context: input.context,
289
+ intent: baseIntent,
290
+ executionEvidence: preview.executionEvidence,
291
+ });
292
+ return {
293
+ provider: 'jira',
294
+ operation: input.operation,
295
+ context: input.context,
296
+ intent: {
297
+ ...baseIntent,
298
+ actionDigest,
299
+ executionEvidence: {
300
+ ...preview.executionEvidence,
301
+ previewDigest: preview.previewDigest,
302
+ approvalDigest: input.approvedPreviewDigest,
303
+ actionDigest,
304
+ },
305
+ },
306
+ };
307
+ }
308
+ export function classifyJiraReadObservation(input) {
309
+ if (!validJiraIssueReadAction(input.action)) {
310
+ return unavailableJiraRead('inaccessible', ['read-action-invalid']);
311
+ }
312
+ if (!Number.isFinite(Date.parse(input.observedAt))) {
313
+ throw new Error('Jira read classification requires a valid timestamp.');
314
+ }
315
+ if (input.outcome === 'temporary-failure') {
316
+ return unavailableJiraRead('temporarily-unavailable', [
317
+ 'temporary-host-failure',
318
+ ]);
319
+ }
320
+ if (input.outcome === 'not-found' || !input.observation) {
321
+ return unavailableJiraRead('inaccessible', [
322
+ input.outcome === 'not-found'
323
+ ? 'absence-not-authoritative'
324
+ : 'read-observation-missing',
325
+ ]);
326
+ }
327
+ const observation = input.observation;
328
+ const exactContext = contextsEqual(input.action.context, observation.context);
329
+ const movedProject = isValidatedJiraProjectMoveEvidence(input.action, observation, input.hostCapability);
330
+ const capabilityContext = movedProject
331
+ ? observation.context
332
+ : input.action.context;
333
+ const capability = validateJiraHostCapability('read', capabilityContext, input.hostCapability, ['stable-identity', 'title', 'status', 'revision', 'project-context']);
334
+ const capabilityBound = movedProject
335
+ ? observation.capabilityEvidenceDigest ===
336
+ input.hostCapability.evidenceDigest &&
337
+ Date.parse(input.hostCapability.observedAt) >=
338
+ Date.parse(String(input.action.intent.capabilityObservedAt))
339
+ : input.action.intent.capabilityEvidenceDigest ===
340
+ input.hostCapability.evidenceDigest &&
341
+ input.action.intent.capabilityObservedAt ===
342
+ input.hostCapability.observedAt;
343
+ if (!capability.valid ||
344
+ observation.provider !== 'jira' ||
345
+ (!exactContext && !movedProject) ||
346
+ !capabilityBound ||
347
+ observation.fields.issueId !== input.action.intent.issueId) {
348
+ return unavailableJiraRead('inaccessible', [
349
+ ...capability.reasons,
350
+ 'observation-attribution-mismatch',
351
+ ]);
352
+ }
353
+ let issue;
354
+ try {
355
+ issue = normalizeJiraIssueObservation(observation);
356
+ }
357
+ catch {
358
+ return unavailableJiraRead('partial', ['read-observation-incomplete']);
359
+ }
360
+ if (issue.stableId !== input.action.intent.stableId) {
361
+ return unavailableJiraRead('inaccessible', [
362
+ 'observation-identity-mismatch',
363
+ ]);
364
+ }
365
+ const currentKey = input.action.intent.currentKey;
366
+ const movedKey = typeof currentKey === 'string' &&
367
+ observation.fields.key !== currentKey &&
368
+ Array.isArray(observation.fields.historicalKeys) &&
369
+ observation.fields.historicalKeys.includes(currentKey);
370
+ if (!['active', 'archived'].includes(String(observation.fields.lifecycle))) {
371
+ return unavailableJiraRead('inaccessible', [
372
+ `lifecycle-unsupported:${String(observation.fields.lifecycle)}`,
373
+ ]);
374
+ }
375
+ const revisionWeak = !observation.revision.token && !observation.revision.updatedAt;
376
+ return {
377
+ classification: observation.fields.lifecycle === 'archived'
378
+ ? 'archived'
379
+ : movedProject || movedKey
380
+ ? 'moved'
381
+ : 'current',
382
+ issue,
383
+ preservePriorEvidence: false,
384
+ reasons: revisionWeak ? ['revision-evidence-weak'] : [],
385
+ };
386
+ }
387
+ export function validateJiraMetadataObservation(input) {
388
+ const reasons = [];
389
+ if (!validJiraMetadataAction(input.action)) {
390
+ reasons.push('metadata-action-invalid');
391
+ }
392
+ const capability = validateJiraHostCapability('read', input.action.context, input.hostCapability, [
393
+ 'metadata',
394
+ ...(input.action.intent.purpose === 'transition'
395
+ ? ['transitions']
396
+ : []),
397
+ ]);
398
+ reasons.push(...capability.reasons);
399
+ if (input.observation.provider !== 'jira' ||
400
+ !contextsEqual(input.action.context, input.observation.context)) {
401
+ reasons.push('observation-context-mismatch');
402
+ }
403
+ if (input.observation.capabilityEvidenceDigest !==
404
+ input.action.intent.capabilityEvidenceDigest ||
405
+ input.hostCapability.evidenceDigest !==
406
+ input.action.intent.capabilityEvidenceDigest ||
407
+ input.hostCapability.observedAt !== input.action.intent.capabilityObservedAt) {
408
+ reasons.push('capability-evidence-mismatch');
409
+ }
410
+ if (input.observation.purpose !== input.action.intent.purpose)
411
+ reasons.push('metadata-purpose-mismatch');
412
+ if (input.observation.availability !== 'available')
413
+ reasons.push(input.observation.availability);
414
+ if (!input.observation.metadataEvidenceDigest)
415
+ reasons.push('metadata-evidence-missing');
416
+ if (input.observation.writableFields.some((field) => !['title', 'description', 'priority', 'status', 'annotation'].includes(field))) {
417
+ reasons.push('metadata-field-invalid');
418
+ }
419
+ return { valid: reasons.length === 0, reasons: uniqueStrings(reasons) };
420
+ }
421
+ export function validateJiraDiscussionReadObservation(input) {
422
+ const invalid = (reasons) => ({
423
+ classification: 'invalid',
424
+ page: null,
425
+ persistable: false,
426
+ reasons,
427
+ });
428
+ if (!validJiraDiscussionAction(input.action) ||
429
+ input.observation.provider !== 'jira' ||
430
+ !contextsEqual(input.action.context, input.observation.context) ||
431
+ input.observation.stableId !== input.action.intent.stableId ||
432
+ input.observation.capabilityEvidenceDigest !==
433
+ input.action.intent.capabilityEvidenceDigest ||
434
+ input.hostCapability.evidenceDigest !==
435
+ input.action.intent.capabilityEvidenceDigest ||
436
+ input.hostCapability.observedAt !==
437
+ input.action.intent.capabilityObservedAt ||
438
+ input.observation.requestedCursor !== input.action.intent.cursor) {
439
+ return invalid(['discussion-evidence-mismatch']);
440
+ }
441
+ const capability = validateJiraHostCapability('read-discussion', input.action.context, input.hostCapability);
442
+ if (!capability.valid)
443
+ return invalid(capability.reasons);
444
+ if (input.observation.availability !== 'available') {
445
+ return {
446
+ classification: input.observation.availability,
447
+ page: null,
448
+ persistable: false,
449
+ reasons: [input.observation.availability],
450
+ };
451
+ }
452
+ const limit = Number(input.action.intent.limit);
453
+ const pageBytes = input.observation.items.reduce((total, item) => total + Buffer.byteLength(item.id + item.body, 'utf8'), 0);
454
+ if (input.observation.items.length > limit ||
455
+ pageBytes > JIRA_DISCUSSION_LIMITS.maxPageBytes ||
456
+ input.observation.items.some((item) => !item.id ||
457
+ Buffer.byteLength(item.id, 'utf8') >
458
+ JIRA_DISCUSSION_LIMITS.maxIdBytes ||
459
+ Buffer.byteLength(item.body, 'utf8') >
460
+ JIRA_DISCUSSION_LIMITS.maxBodyBytes ||
461
+ !Number.isFinite(Date.parse(item.observedAt))) ||
462
+ (input.observation.nextCursor !== null &&
463
+ Buffer.byteLength(input.observation.nextCursor, 'utf8') >
464
+ JIRA_DISCUSSION_LIMITS.maxCursorBytes)) {
465
+ return invalid(['discussion-page-out-of-bounds']);
466
+ }
467
+ return {
468
+ classification: 'page',
469
+ page: {
470
+ items: input.observation.items.map((item) => {
471
+ const contentSuppressed = containsSensitiveContentSignal(item.body);
472
+ return {
473
+ ...item,
474
+ body: contentSuppressed ? WHOLE_FIELD_SUPPRESSION_MARKER : item.body,
475
+ contentSuppressed,
476
+ };
477
+ }),
478
+ nextCursor: input.observation.nextCursor,
479
+ },
480
+ persistable: false,
481
+ reasons: [],
482
+ };
483
+ }
484
+ export function verifyJiraMutationObservation(input) {
485
+ const fields = mutationVerificationFields(input.action);
486
+ const terminal = (classification, reason, statuses = fields.map((field) => ({
487
+ field,
488
+ status: 'unavailable',
489
+ }))) => ({
490
+ classification,
491
+ reason,
492
+ fields: statuses,
493
+ retryAllowed: false,
494
+ });
495
+ if (!validJiraMutationAction(input.action))
496
+ return terminal('uncertain', 'mutation-action-invalid');
497
+ if (input.attempt.count !== 1)
498
+ return terminal('uncertain', 'attempt-count-invalid');
499
+ if (input.attempt.outcome === 'rejected')
500
+ return terminal('rejected', 'provider-rejected');
501
+ if (input.attempt.outcome === 'unknown' || !input.readback) {
502
+ return terminal('uncertain', 'authoritative-readback-required');
503
+ }
504
+ const fieldMask = input.action.intent.fieldMask;
505
+ const currentCapability = validateJiraHostCapability(input.action.operation, input.action.context, input.hostCapability);
506
+ let currentMetadataContract;
507
+ try {
508
+ currentMetadataContract = normalizeJiraMetadataContract(input.normalizedMetadata);
509
+ }
510
+ catch {
511
+ return terminal('uncertain', 'current-metadata-invalid');
512
+ }
513
+ if (!currentCapability.valid ||
514
+ (fieldMask.includes('description') &&
515
+ !input.hostCapability.semanticCapabilities.includes('structural-description-write')) ||
516
+ !jiraMetadataSupports({
517
+ operation: input.action
518
+ .operation,
519
+ normalizedMetadata: input.normalizedMetadata,
520
+ projection: input.action.intent.projection,
521
+ }, fieldMask) ||
522
+ !semanticValuesEqual(currentMetadataContract, input.action.intent.metadataContract) ||
523
+ semanticDigest(currentMetadataContract) !==
524
+ input.action.intent.metadataSemanticsDigest) {
525
+ return terminal('uncertain', 'current-capability-or-metadata-semantics-invalid');
526
+ }
527
+ if (input.attempt.capabilityEvidenceDigest !==
528
+ input.action.intent.capabilityEvidenceDigest ||
529
+ input.hostCapability.evidenceDigest !==
530
+ input.action.intent.capabilityEvidenceDigest ||
531
+ input.hostCapability.observedAt !==
532
+ (isRecord(input.action.intent.executionEvidence)
533
+ ? input.action.intent.executionEvidence.capabilityObservedAt
534
+ : null) ||
535
+ input.attempt.metadataEvidenceDigest !==
536
+ input.action.intent.metadataEvidenceDigest ||
537
+ input.normalizedMetadata.evidenceDigest !==
538
+ input.action.intent.metadataEvidenceDigest) {
539
+ return terminal('uncertain', 'execution-evidence-mismatch');
540
+ }
541
+ const publicValidation = jiraAdapter.validateObservation(input.action, input.readback);
542
+ if (!publicValidation.valid)
543
+ return terminal('uncertain', publicValidation.reasons.join(','));
544
+ let issue;
545
+ try {
546
+ issue = normalizeJiraIssueObservation(input.readback);
547
+ }
548
+ catch {
549
+ return terminal('uncertain', 'readback-invalid');
550
+ }
551
+ if (input.action.operation !== 'create' &&
552
+ issue.stableId !== input.action.intent.stableId) {
553
+ return terminal('uncertain', 'readback-identity-mismatch');
554
+ }
555
+ const postconditions = input.action.intent.postconditions;
556
+ const statuses = fields.map((field) => {
557
+ const observed = field === 'description'
558
+ ? issue.description
559
+ : field === 'status'
560
+ ? issue.status
561
+ : field === 'annotation'
562
+ ? issue.extensions.annotations
563
+ : issue[field];
564
+ const verified = field === 'description'
565
+ ? semanticValuesEqual(observed, postconditions[field]) &&
566
+ verifyJiraDescriptionReadback(input.action, issue)
567
+ : field === 'annotation' && Array.isArray(observed)
568
+ ? observed.includes(postconditions[field])
569
+ : semanticValuesEqual(observed, postconditions[field]);
570
+ return {
571
+ field,
572
+ status: verified ? 'verified' : 'mismatch',
573
+ };
574
+ });
575
+ return statuses.every((field) => field.status === 'verified')
576
+ ? terminal('verified', 'authoritative-readback-matched', statuses)
577
+ : terminal('partial', 'postcondition-mismatch', statuses);
578
+ }
579
+ export function planJiraDuplicateSearch(input) {
580
+ requireJiraCapability(input.hostCapability, input.context, 'search-duplicates', ['stable-identity', 'project-context']);
581
+ if (!input.provenanceToken ||
582
+ Buffer.byteLength(input.provenanceToken, 'utf8') > 512 ||
583
+ !input.reservedBindingId ||
584
+ Buffer.byteLength(input.reservedBindingId, 'utf8') > 128 ||
585
+ input.historicalKeys.length > 64 ||
586
+ new Set(input.historicalKeys).size !== input.historicalKeys.length ||
587
+ input.historicalKeys.some((key) => !ISSUE_KEY.test(key)) ||
588
+ !Number.isInteger(input.maxResults) ||
589
+ input.maxResults < 1 ||
590
+ input.maxResults > 100) {
591
+ throw new Error('Jira duplicate search bounds are invalid.');
592
+ }
593
+ const query = {
594
+ provenanceToken: input.provenanceToken,
595
+ reservedBindingId: input.reservedBindingId,
596
+ historicalKeys: [...input.historicalKeys],
597
+ siteId: input.context.siteId,
598
+ projectId: input.context.projectId,
599
+ };
600
+ return jiraAction('search-duplicates', input.context, {
601
+ query,
602
+ queryDigest: semanticDigest(query),
603
+ capabilityEvidenceDigest: input.hostCapability.evidenceDigest,
604
+ capabilityObservedAt: input.hostCapability.observedAt,
605
+ resultContract: {
606
+ maxResults: input.maxResults,
607
+ classifications: ['no-match', 'one-match', 'ambiguous'],
608
+ requireStableIssueId: true,
609
+ requireExactContext: true,
610
+ matchStatus: 'evidence-until-identity-and-context-verified',
611
+ },
612
+ });
613
+ }
614
+ export function validateJiraDuplicateSearchObservation(input) {
615
+ const invalid = (classification, reasons) => ({
616
+ accepted: classification === 'no-match',
617
+ classification,
618
+ stableId: null,
619
+ reasons,
620
+ });
621
+ if (!validJiraDuplicateAction(input.action)) {
622
+ return invalid('invalid', ['search-action-invalid']);
623
+ }
624
+ const capability = validateJiraHostCapability('search-duplicates', input.action.context, input.hostCapability, ['stable-identity', 'project-context']);
625
+ if (!capability.valid ||
626
+ input.hostCapability.evidenceDigest !==
627
+ input.action.intent.capabilityEvidenceDigest ||
628
+ input.hostCapability.observedAt !==
629
+ input.action.intent.capabilityObservedAt ||
630
+ input.observation.provider !== 'jira' ||
631
+ !contextsEqual(input.observation.context, input.action.context) ||
632
+ input.observation.capabilityEvidenceDigest !==
633
+ input.action.intent.capabilityEvidenceDigest ||
634
+ input.observation.queryDigest !== input.action.intent.queryDigest ||
635
+ !Number.isFinite(Date.parse(input.observation.observedAt))) {
636
+ return invalid('invalid', [
637
+ 'search-evidence-mismatch',
638
+ ...capability.reasons,
639
+ ]);
640
+ }
641
+ if (input.observation.availability === 'unavailable') {
642
+ return invalid('unavailable', ['search-unavailable']);
643
+ }
644
+ if (input.observation.availability === 'lagging') {
645
+ return invalid('lagging', ['search-result-lag']);
646
+ }
647
+ const maxResults = Number(input.action.intent.resultContract.maxResults);
648
+ if (input.observation.results.length > maxResults) {
649
+ return invalid('invalid', ['search-results-out-of-bounds']);
650
+ }
651
+ const query = input.action.intent.query;
652
+ const historicalKeys = query.historicalKeys;
653
+ for (const candidate of input.observation.results) {
654
+ if (!ISSUE_ID.test(candidate.issueId) ||
655
+ candidate.stableId !==
656
+ canonicalJiraStableId(input.action.context.siteId, candidate.issueId) ||
657
+ candidate.keys.length > 64 ||
658
+ new Set(candidate.keys).size !== candidate.keys.length ||
659
+ candidate.keys.some((key) => !ISSUE_KEY.test(key)) ||
660
+ !candidate.stableIdentityVerified ||
661
+ !candidate.contextVerified ||
662
+ !contextsEqual(candidate.context, input.action.context)) {
663
+ return invalid('invalid', ['candidate-identity-or-context-unverified']);
664
+ }
665
+ const matched = (candidate.matchedBy === 'provenance' &&
666
+ candidate.matchedProvenanceToken === query.provenanceToken) ||
667
+ (candidate.matchedBy === 'reserved-binding' &&
668
+ candidate.matchedReservedBindingId === query.reservedBindingId) ||
669
+ (candidate.matchedBy === 'historical-key' &&
670
+ typeof candidate.matchedHistoricalKey === 'string' &&
671
+ historicalKeys.includes(candidate.matchedHistoricalKey) &&
672
+ candidate.keys.includes(candidate.matchedHistoricalKey));
673
+ if (!matched) {
674
+ return invalid('invalid', ['candidate-match-evidence-invalid']);
675
+ }
676
+ }
677
+ if (input.observation.results.length === 0)
678
+ return invalid('no-match', []);
679
+ if (input.observation.results.length > 1) {
680
+ return invalid('ambiguous', ['multiple-matches']);
681
+ }
682
+ return {
683
+ accepted: true,
684
+ classification: 'one-verified-match',
685
+ stableId: input.observation.results[0].stableId,
686
+ reasons: [],
687
+ };
688
+ }
689
+ function validateJiraPublicObservation(action, observation) {
690
+ if (action.operation === 'read-discussion' ||
691
+ action.operation === 'search-duplicates') {
692
+ return {
693
+ valid: false,
694
+ reasons: [`typed-validator-required:${action.operation}`],
695
+ };
696
+ }
697
+ const mutation = ['create', 'update', 'transition', 'annotate'].includes(action.operation);
698
+ const validAction = mutation
699
+ ? validJiraMutationAction(action)
700
+ : action.intent.kind === 'issue'
701
+ ? validJiraIssueReadAction(action)
702
+ : action.intent.kind === 'metadata'
703
+ ? validJiraMetadataAction(action)
704
+ : false;
705
+ if (!validAction)
706
+ return { valid: false, reasons: ['action-evidence-invalid'] };
707
+ if (action.provider !== 'jira' || observation.provider !== 'jira') {
708
+ return { valid: false, reasons: ['provider-mismatch'] };
709
+ }
710
+ const capability = parseJiraHostCapability(observation.fields.hostCapability);
711
+ if (!capability) {
712
+ return { valid: false, reasons: ['capability-evidence-missing'] };
713
+ }
714
+ if (action.operation === 'read' && action.intent.kind === 'issue') {
715
+ const result = classifyJiraReadObservation({
716
+ action,
717
+ hostCapability: capability,
718
+ observedAt: capability.observedAt,
719
+ outcome: 'found',
720
+ observation,
721
+ });
722
+ return result.issue
723
+ ? { valid: true, reasons: [] }
724
+ : { valid: false, reasons: result.reasons };
725
+ }
726
+ const reasons = [];
727
+ if (!contextsEqual(action.context, observation.context)) {
728
+ reasons.push('context-mismatch');
729
+ }
730
+ const requiredFields = action.operation === 'read' && action.intent.kind === 'metadata'
731
+ ? [
732
+ 'metadata',
733
+ ...(action.intent.purpose === 'transition'
734
+ ? ['transitions']
735
+ : []),
736
+ ]
737
+ : [];
738
+ const capabilityValidation = validateJiraHostCapability(action.operation, action.context, capability, requiredFields);
739
+ reasons.push(...capabilityValidation.reasons);
740
+ if (mutation &&
741
+ Array.isArray(action.intent.fieldMask) &&
742
+ action.intent.fieldMask.includes('description') &&
743
+ !capability.semanticCapabilities.includes('structural-description-write')) {
744
+ reasons.push('semantic-capability-missing:structural-description-write');
745
+ }
746
+ const expectedCapabilityObservedAt = mutation
747
+ ? isRecord(action.intent.executionEvidence)
748
+ ? action.intent.executionEvidence.capabilityObservedAt
749
+ : null
750
+ : action.intent.capabilityObservedAt;
751
+ if (observation.capabilityEvidenceDigest !==
752
+ action.intent.capabilityEvidenceDigest ||
753
+ capability.evidenceDigest !== action.intent.capabilityEvidenceDigest ||
754
+ capability.observedAt !== expectedCapabilityObservedAt) {
755
+ reasons.push('capability-evidence-mismatch');
756
+ }
757
+ if (action.operation === 'read' &&
758
+ action.intent.kind === 'metadata' &&
759
+ typeof observation.fields.metadataEvidenceDigest !== 'string') {
760
+ reasons.push('metadata-evidence-missing');
761
+ }
762
+ if (mutation) {
763
+ if (!semanticValuesEqual(observation.fields.mutationEvidence, action.intent.executionEvidence)) {
764
+ reasons.push('mutation-evidence-mismatch');
765
+ }
766
+ if (action.operation === 'create' &&
767
+ !semanticValuesEqual(observation.fields.createProvenance, action.intent.provenance)) {
768
+ reasons.push('create-provenance-mismatch');
769
+ }
770
+ try {
771
+ const issue = normalizeJiraIssueObservation(observation);
772
+ if (action.operation !== 'create' &&
773
+ issue.stableId !== action.intent.stableId) {
774
+ reasons.push('observation-identity-mismatch');
775
+ }
776
+ if (Array.isArray(action.intent.fieldMask) &&
777
+ action.intent.fieldMask.includes('description') &&
778
+ !verifyJiraDescriptionReadback(action, issue)) {
779
+ reasons.push('description-adf-readback-mismatch');
780
+ }
781
+ }
782
+ catch {
783
+ reasons.push('observation-invalid');
784
+ }
785
+ }
786
+ return { valid: reasons.length === 0, reasons: uniqueStrings(reasons) };
787
+ }
788
+ export const jiraAdapter = {
789
+ provider: 'jira',
790
+ normalize: normalizeJiraIssueObservation,
791
+ plan(operation, input) {
792
+ if (['create', 'update', 'transition', 'annotate'].includes(operation)) {
793
+ if (input.operation !== operation) {
794
+ throw new Error('Jira mutation operation selector does not match.');
795
+ }
796
+ return planJiraMutation(input);
797
+ }
798
+ if (operation === 'read' && input.kind === 'metadata') {
799
+ return planJiraMetadataRead(input);
800
+ }
801
+ if (operation === 'read')
802
+ return planJiraRead(input);
803
+ if (operation === 'read-discussion') {
804
+ return planJiraDiscussionRead(input);
805
+ }
806
+ if (operation === 'search-duplicates') {
807
+ return planJiraDuplicateSearch(input);
808
+ }
809
+ return {
810
+ provider: 'jira',
811
+ operation,
812
+ context: (input.context ?? {}),
813
+ intent: input,
814
+ };
815
+ },
816
+ validateObservation(action, observation) {
817
+ return validateJiraPublicObservation(action, observation);
818
+ },
819
+ verificationFields(action) {
820
+ if (action.operation === 'search-duplicates')
821
+ return ['typed:search-duplicates'];
822
+ return action.operation === 'read'
823
+ ? ['stable-identity']
824
+ : mutationVerificationFields(action);
825
+ },
826
+ verify(action, issue) {
827
+ if (action.operation === 'search-duplicates') {
828
+ return [{ field: 'typed:search-duplicates', status: 'unavailable' }];
829
+ }
830
+ if (['create', 'update', 'transition', 'annotate'].includes(action.operation)) {
831
+ const fields = mutationVerificationFields(action);
832
+ if (!validJiraMutationAction(action) ||
833
+ issue.provider !== 'jira' ||
834
+ !contextsEqual(action.context, issue.context) ||
835
+ (action.operation !== 'create' &&
836
+ issue.stableId !== action.intent.stableId) ||
837
+ issue.extensions.capabilityEvidenceDigest !==
838
+ action.intent.capabilityEvidenceDigest ||
839
+ !semanticValuesEqual(issue.extensions.mutationEvidence, action.intent.executionEvidence) ||
840
+ (action.operation === 'create' &&
841
+ !semanticValuesEqual(issue.extensions.createProvenance, action.intent.provenance))) {
842
+ return fields.map((field) => ({ field, status: 'unavailable' }));
843
+ }
844
+ const postconditions = action.intent.postconditions;
845
+ return fields.map((field) => {
846
+ const observed = field === 'description'
847
+ ? issue.description
848
+ : field === 'status'
849
+ ? issue.status
850
+ : field === 'annotation'
851
+ ? issue.extensions.annotations
852
+ : issue[field];
853
+ return {
854
+ field,
855
+ status: field === 'description'
856
+ ? semanticValuesEqual(observed, postconditions[field]) &&
857
+ verifyJiraDescriptionReadback(action, issue)
858
+ ? 'verified'
859
+ : 'mismatch'
860
+ : field === 'annotation' && Array.isArray(observed)
861
+ ? observed.includes(postconditions[field])
862
+ ? 'verified'
863
+ : 'mismatch'
864
+ : semanticValuesEqual(observed, postconditions[field])
865
+ ? 'verified'
866
+ : 'mismatch',
867
+ };
868
+ });
869
+ }
870
+ return this.verificationFields(action).map((field) => ({
871
+ field,
872
+ status: validJiraIssueReadAction(action) &&
873
+ action.operation === 'read' &&
874
+ action.intent.kind === 'issue' &&
875
+ issue.provider === 'jira' &&
876
+ issue.stableId === action.intent.stableId &&
877
+ ((contextsEqual(action.context, issue.context) &&
878
+ issue.extensions.capabilityEvidenceDigest ===
879
+ action.intent.capabilityEvidenceDigest) ||
880
+ isNormalizedJiraProjectMove(action, issue))
881
+ ? 'verified'
882
+ : 'unavailable',
883
+ }));
884
+ },
885
+ };
886
+ function canonicalJiraStableId(siteId, issueId) {
887
+ return `jira:${siteId}:${issueId}`;
888
+ }
889
+ function hasPinnedJiraContext(context) {
890
+ return ['accountId', 'siteId', 'projectId'].every((key) => typeof context[key] === 'string' && context[key].length > 0);
891
+ }
892
+ function validJiraStableIdForContext(stableId, context) {
893
+ if (!hasPinnedJiraContext(context))
894
+ return false;
895
+ const prefix = `jira:${context.siteId}:`;
896
+ return (stableId.startsWith(prefix) && ISSUE_ID.test(stableId.slice(prefix.length)));
897
+ }
898
+ function isValidatedJiraProjectMoveEvidence(action, observation, capability) {
899
+ const oldProjectId = action.context.projectId;
900
+ const newProjectId = observation.context.projectId;
901
+ const currentKey = action.intent.currentKey;
902
+ return (action.provider === 'jira' &&
903
+ observation.provider === 'jira' &&
904
+ action.context.accountId === observation.context.accountId &&
905
+ action.context.siteId === observation.context.siteId &&
906
+ typeof oldProjectId === 'string' &&
907
+ typeof newProjectId === 'string' &&
908
+ oldProjectId !== newProjectId &&
909
+ observation.fields.issueId === action.intent.issueId &&
910
+ Array.isArray(observation.fields.historicalProjectIds) &&
911
+ observation.fields.historicalProjectIds.includes(oldProjectId) &&
912
+ (currentKey === null ||
913
+ (typeof currentKey === 'string' &&
914
+ observation.fields.key !== currentKey &&
915
+ Array.isArray(observation.fields.historicalKeys) &&
916
+ observation.fields.historicalKeys.includes(currentKey))) &&
917
+ contextsEqual(capability.context, observation.context) &&
918
+ capability.accountId === observation.context.accountId &&
919
+ capability.siteId === observation.context.siteId &&
920
+ capability.projectId === newProjectId &&
921
+ capability.evidenceDigest === observation.capabilityEvidenceDigest);
922
+ }
923
+ function normalizedJiraCapabilityEvidence(observation) {
924
+ const capability = parseJiraHostCapability(observation.fields.hostCapability);
925
+ if (!capability ||
926
+ capability.evidenceDigest !== observation.capabilityEvidenceDigest ||
927
+ !validateJiraHostCapability('read', observation.context, capability, [
928
+ 'stable-identity',
929
+ 'title',
930
+ 'status',
931
+ 'revision',
932
+ 'project-context',
933
+ ]).valid) {
934
+ return null;
935
+ }
936
+ return {
937
+ accountId: capability.accountId,
938
+ siteId: capability.siteId,
939
+ projectId: capability.projectId,
940
+ evidenceDigest: capability.evidenceDigest,
941
+ observedAt: capability.observedAt,
942
+ };
943
+ }
944
+ function isNormalizedJiraProjectMove(action, issue) {
945
+ const evidence = issue.extensions.validatedCapabilityEvidence;
946
+ const currentKey = action.intent.currentKey;
947
+ return (action.context.accountId === issue.context.accountId &&
948
+ action.context.siteId === issue.context.siteId &&
949
+ action.context.projectId !== issue.context.projectId &&
950
+ Array.isArray(issue.extensions.historicalProjectIds) &&
951
+ issue.extensions.historicalProjectIds.includes(action.context.projectId) &&
952
+ (currentKey === null ||
953
+ (typeof currentKey === 'string' &&
954
+ issue.extensions.currentKey !== currentKey &&
955
+ Array.isArray(issue.extensions.historicalKeys) &&
956
+ issue.extensions.historicalKeys.includes(currentKey))) &&
957
+ typeof issue.extensions.capabilityEvidenceDigest === 'string' &&
958
+ isRecord(evidence) &&
959
+ hasExactKeys(evidence, [
960
+ 'accountId',
961
+ 'siteId',
962
+ 'projectId',
963
+ 'evidenceDigest',
964
+ 'observedAt',
965
+ ]) &&
966
+ evidence.accountId === issue.context.accountId &&
967
+ evidence.siteId === issue.context.siteId &&
968
+ evidence.projectId === issue.context.projectId &&
969
+ evidence.evidenceDigest === issue.extensions.capabilityEvidenceDigest &&
970
+ typeof evidence.observedAt === 'string' &&
971
+ Number.isFinite(Date.parse(evidence.observedAt)) &&
972
+ Date.parse(evidence.observedAt) >=
973
+ Date.parse(String(action.intent.capabilityObservedAt)));
974
+ }
975
+ function requireJiraCapability(capability, context, operation, fields = []) {
976
+ const validation = validateJiraHostCapability(operation, context, capability, fields);
977
+ if (!validation.valid) {
978
+ throw new Error(`Jira semantic capability is unavailable: ${validation.reasons.join(',')}`);
979
+ }
980
+ }
981
+ function requireJiraSemanticCapability(capability, required) {
982
+ if (!capability.semanticCapabilities.includes(required)) {
983
+ throw new Error(`Jira semantic capability is unavailable: ${required}`);
984
+ }
985
+ }
986
+ function assertJiraMutationIdentity(input) {
987
+ if (!hasPinnedJiraContext(input.context) || !input.bindingId) {
988
+ throw new Error('Jira mutation requires a binding and pinned context.');
989
+ }
990
+ if (input.operation === 'create') {
991
+ if (input.stableId !== undefined ||
992
+ input.provenance?.bindingId !== input.bindingId ||
993
+ !input.provenance.origin) {
994
+ throw new Error('Jira create requires exact creation provenance.');
995
+ }
996
+ }
997
+ else if (!input.stableId ||
998
+ !validJiraStableIdForContext(input.stableId, input.context)) {
999
+ throw new Error('Jira mutation requires a stable issue identity.');
1000
+ }
1001
+ }
1002
+ function normalizeJiraMutationFieldMask(input) {
1003
+ const allowed = {
1004
+ create: ['title', 'description', 'priority'],
1005
+ update: ['title', 'description', 'priority'],
1006
+ transition: ['status'],
1007
+ annotate: ['annotation'],
1008
+ };
1009
+ if (input.fieldMask.length === 0 ||
1010
+ new Set(input.fieldMask).size !== input.fieldMask.length ||
1011
+ input.fieldMask.some((field) => !allowed[input.operation].includes(field)) ||
1012
+ Object.keys(input.projection).length !== input.fieldMask.length ||
1013
+ Object.keys(input.projection).some((field) => !input.fieldMask.includes(field))) {
1014
+ throw new Error('Jira mutation projection or field mask is invalid.');
1015
+ }
1016
+ return [...input.fieldMask];
1017
+ }
1018
+ function assertJiraMetadataSupports(input, fieldMask) {
1019
+ normalizeJiraMetadataContract(input.normalizedMetadata);
1020
+ if (!input.normalizedMetadata.evidenceDigest) {
1021
+ throw new Error('Jira normalized metadata evidence is missing.');
1022
+ }
1023
+ if (!jiraMetadataSupports(input, fieldMask)) {
1024
+ throw new Error(input.operation === 'transition'
1025
+ ? 'Jira transition or requested field is unavailable in normalized metadata.'
1026
+ : 'Jira normalized metadata does not allow requested fields.');
1027
+ }
1028
+ }
1029
+ function jiraMetadataSupports(input, fieldMask) {
1030
+ return (fieldMask.every((field) => input.normalizedMetadata.writableFields.includes(field)) &&
1031
+ (input.operation !== 'transition' ||
1032
+ input.normalizedMetadata.transitions.includes(String(input.projection.status))));
1033
+ }
1034
+ function normalizeJiraMetadataContract(metadata) {
1035
+ if (!Array.isArray(metadata.writableFields) ||
1036
+ metadata.writableFields.length > 5 ||
1037
+ new Set(metadata.writableFields).size !== metadata.writableFields.length ||
1038
+ metadata.writableFields.some((field) => !['title', 'description', 'priority', 'status', 'annotation'].includes(field)) ||
1039
+ !Array.isArray(metadata.transitions) ||
1040
+ metadata.transitions.length > 256 ||
1041
+ new Set(metadata.transitions).size !== metadata.transitions.length ||
1042
+ metadata.transitions.some((transition) => typeof transition !== 'string' ||
1043
+ !transition ||
1044
+ Buffer.byteLength(transition, 'utf8') > 255)) {
1045
+ throw new Error('Jira normalized metadata semantics are invalid.');
1046
+ }
1047
+ return {
1048
+ writableFields: [...metadata.writableFields].sort(),
1049
+ transitions: [...metadata.transitions].sort(),
1050
+ };
1051
+ }
1052
+ function buildJiraDescriptionAdfEvidence(input, fieldMask) {
1053
+ if (!fieldMask.includes('description')) {
1054
+ if (input.descriptionAdf !== undefined) {
1055
+ throw new Error('Jira ADF evidence is allowed only for description mutations.');
1056
+ }
1057
+ return null;
1058
+ }
1059
+ const evidence = input.descriptionAdf;
1060
+ const expectedText = input.projection.description;
1061
+ if (!evidence ||
1062
+ evidence.bindingId !== input.bindingId ||
1063
+ typeof expectedText !== 'string') {
1064
+ throw new Error('Jira description mutation requires binding-matched ADF evidence.');
1065
+ }
1066
+ validateJiraAdfDocument(evidence.before);
1067
+ validateJiraAdfDocument(evidence.after);
1068
+ if (!verifyJiraAdfReplacement(evidence.before, evidence.after, evidence.bindingId, expectedText)) {
1069
+ throw new Error('Jira description ADF transition is not structurally preserved.');
1070
+ }
1071
+ const before = inspectJiraAdf(evidence.before, evidence.bindingId);
1072
+ const after = inspectJiraAdf(evidence.after, evidence.bindingId);
1073
+ return {
1074
+ bindingId: evidence.bindingId,
1075
+ beforeDocumentDigest: before.documentDigest,
1076
+ afterDocumentDigest: after.documentDigest,
1077
+ surroundingDigest: after.surroundingDigest,
1078
+ managedTextDigest: semanticDigest(expectedText),
1079
+ };
1080
+ }
1081
+ function buildJiraStructuralDescriptionWrite(input, evidence) {
1082
+ if (evidence === null)
1083
+ return null;
1084
+ const document = structuredClone(input.descriptionAdf.after);
1085
+ return {
1086
+ kind: 'replace-managed-description',
1087
+ bindingId: evidence.bindingId,
1088
+ document,
1089
+ documentDigest: evidence.afterDocumentDigest,
1090
+ beforeDocumentDigest: evidence.beforeDocumentDigest,
1091
+ surroundingDigest: evidence.surroundingDigest,
1092
+ managedTextDigest: evidence.managedTextDigest,
1093
+ };
1094
+ }
1095
+ function jiraAction(operation, context, intent) {
1096
+ return {
1097
+ provider: 'jira',
1098
+ operation,
1099
+ context,
1100
+ intent: {
1101
+ ...intent,
1102
+ actionDigest: semanticDigest({
1103
+ provider: 'jira',
1104
+ operation,
1105
+ context,
1106
+ intent,
1107
+ }),
1108
+ },
1109
+ };
1110
+ }
1111
+ function mutationVerificationFields(action) {
1112
+ return Array.isArray(action.intent.fieldMask)
1113
+ ? action.intent.fieldMask.filter((field) => typeof field === 'string')
1114
+ : [];
1115
+ }
1116
+ function jiraIssueResultContract() {
1117
+ return {
1118
+ requireStableIdentity: true,
1119
+ requireExactContext: true,
1120
+ allowedFields: [
1121
+ 'issueId',
1122
+ 'key',
1123
+ 'historicalKeys',
1124
+ 'siteId',
1125
+ 'projectId',
1126
+ 'historicalProjectIds',
1127
+ 'title',
1128
+ 'descriptionText',
1129
+ 'descriptionAdf',
1130
+ 'status',
1131
+ 'priority',
1132
+ 'lifecycle',
1133
+ ...JIRA_EXTENSION_KEYS,
1134
+ ],
1135
+ };
1136
+ }
1137
+ function jiraMetadataResultContract(purpose) {
1138
+ return {
1139
+ normalizedOnly: true,
1140
+ requireExactContext: true,
1141
+ fields: purpose === 'transition' ? ['transitions'] : ['fields', 'issueTypes'],
1142
+ };
1143
+ }
1144
+ function jiraDiscussionResultContract(limit) {
1145
+ return {
1146
+ maxItems: limit,
1147
+ maxIdBytes: JIRA_DISCUSSION_LIMITS.maxIdBytes,
1148
+ maxBodyBytes: JIRA_DISCUSSION_LIMITS.maxBodyBytes,
1149
+ maxCursorBytes: JIRA_DISCUSSION_LIMITS.maxCursorBytes,
1150
+ maxPageBytes: JIRA_DISCUSSION_LIMITS.maxPageBytes,
1151
+ persistable: false,
1152
+ contentPolicy: 'sanitized-whole-field-suppression',
1153
+ };
1154
+ }
1155
+ function validJiraIssueReadAction(action) {
1156
+ if (action.provider !== 'jira' ||
1157
+ action.operation !== 'read' ||
1158
+ !hasPinnedJiraContext(action.context) ||
1159
+ !hasExactKeys(action.intent, [
1160
+ 'kind',
1161
+ 'stableId',
1162
+ 'issueId',
1163
+ 'currentKey',
1164
+ 'stepId',
1165
+ 'capabilityEvidenceDigest',
1166
+ 'capabilityObservedAt',
1167
+ 'resultContract',
1168
+ 'actionDigest',
1169
+ ]) ||
1170
+ action.intent.kind !== 'issue' ||
1171
+ typeof action.intent.issueId !== 'string' ||
1172
+ !ISSUE_ID.test(action.intent.issueId) ||
1173
+ action.intent.stableId !==
1174
+ canonicalJiraStableId(action.context.siteId, action.intent.issueId) ||
1175
+ (action.intent.currentKey !== null &&
1176
+ (typeof action.intent.currentKey !== 'string' ||
1177
+ !ISSUE_KEY.test(action.intent.currentKey))) ||
1178
+ typeof action.intent.stepId !== 'string' ||
1179
+ !action.intent.stepId ||
1180
+ Buffer.byteLength(action.intent.stepId, 'utf8') > 128 ||
1181
+ typeof action.intent.capabilityEvidenceDigest !== 'string' ||
1182
+ !action.intent.capabilityEvidenceDigest ||
1183
+ typeof action.intent.capabilityObservedAt !== 'string' ||
1184
+ !Number.isFinite(Date.parse(action.intent.capabilityObservedAt)) ||
1185
+ !semanticValuesEqual(action.intent.resultContract, jiraIssueResultContract())) {
1186
+ return false;
1187
+ }
1188
+ return validJiraActionDigest(action);
1189
+ }
1190
+ function validJiraMetadataAction(action) {
1191
+ const purpose = action.intent.purpose;
1192
+ if (action.provider !== 'jira' ||
1193
+ action.operation !== 'read' ||
1194
+ !hasPinnedJiraContext(action.context) ||
1195
+ !hasExactKeys(action.intent, [
1196
+ 'kind',
1197
+ 'purpose',
1198
+ 'issueId',
1199
+ 'capabilityEvidenceDigest',
1200
+ 'capabilityObservedAt',
1201
+ 'resultContract',
1202
+ 'actionDigest',
1203
+ ]) ||
1204
+ action.intent.kind !== 'metadata' ||
1205
+ !['create', 'update', 'transition'].includes(String(purpose)) ||
1206
+ (action.intent.issueId !== null &&
1207
+ (typeof action.intent.issueId !== 'string' ||
1208
+ !ISSUE_ID.test(action.intent.issueId))) ||
1209
+ typeof action.intent.capabilityEvidenceDigest !== 'string' ||
1210
+ !action.intent.capabilityEvidenceDigest ||
1211
+ typeof action.intent.capabilityObservedAt !== 'string' ||
1212
+ !Number.isFinite(Date.parse(action.intent.capabilityObservedAt)) ||
1213
+ !semanticValuesEqual(action.intent.resultContract, jiraMetadataResultContract(purpose))) {
1214
+ return false;
1215
+ }
1216
+ return validJiraActionDigest(action);
1217
+ }
1218
+ function validJiraDiscussionAction(action) {
1219
+ const limit = Number(action.intent.limit);
1220
+ if (action.provider !== 'jira' ||
1221
+ action.operation !== 'read-discussion' ||
1222
+ !hasPinnedJiraContext(action.context) ||
1223
+ !hasExactKeys(action.intent, [
1224
+ 'stableId',
1225
+ 'cursor',
1226
+ 'limit',
1227
+ 'capabilityEvidenceDigest',
1228
+ 'capabilityObservedAt',
1229
+ 'resultContract',
1230
+ 'actionDigest',
1231
+ ]) ||
1232
+ typeof action.intent.stableId !== 'string' ||
1233
+ !validJiraStableIdForContext(action.intent.stableId, action.context) ||
1234
+ (action.intent.cursor !== null &&
1235
+ (typeof action.intent.cursor !== 'string' ||
1236
+ Buffer.byteLength(action.intent.cursor, 'utf8') >
1237
+ JIRA_DISCUSSION_LIMITS.maxCursorBytes)) ||
1238
+ !Number.isInteger(limit) ||
1239
+ limit < 1 ||
1240
+ limit > JIRA_DISCUSSION_LIMITS.maxItems ||
1241
+ typeof action.intent.capabilityEvidenceDigest !== 'string' ||
1242
+ !action.intent.capabilityEvidenceDigest ||
1243
+ typeof action.intent.capabilityObservedAt !== 'string' ||
1244
+ !Number.isFinite(Date.parse(action.intent.capabilityObservedAt)) ||
1245
+ !semanticValuesEqual(action.intent.resultContract, jiraDiscussionResultContract(limit))) {
1246
+ return false;
1247
+ }
1248
+ return validJiraActionDigest(action);
1249
+ }
1250
+ function validJiraMutationAction(action) {
1251
+ if (action.provider !== 'jira' ||
1252
+ !['create', 'update', 'transition', 'annotate'].includes(action.operation) ||
1253
+ !hasPinnedJiraContext(action.context)) {
1254
+ return false;
1255
+ }
1256
+ const intent = action.intent;
1257
+ if (!hasExactKeys(intent, [
1258
+ 'bindingId',
1259
+ 'stableId',
1260
+ 'provenance',
1261
+ 'fieldMask',
1262
+ 'projection',
1263
+ 'postconditions',
1264
+ 'capabilityEvidenceDigest',
1265
+ 'metadataEvidenceDigest',
1266
+ 'metadataSemanticsDigest',
1267
+ 'metadataContract',
1268
+ 'structuralDescriptionWrite',
1269
+ 'requiredSemanticCapabilities',
1270
+ 'outboundSafety',
1271
+ 'outboundSafetyEvidence',
1272
+ 'previewDigest',
1273
+ 'approvalDigest',
1274
+ 'readbackContract',
1275
+ 'actionDigest',
1276
+ 'executionEvidence',
1277
+ ]) ||
1278
+ typeof intent.bindingId !== 'string' ||
1279
+ !intent.bindingId ||
1280
+ !Array.isArray(intent.fieldMask) ||
1281
+ !isRecord(intent.projection) ||
1282
+ !isRecord(intent.postconditions) ||
1283
+ !semanticValuesEqual(intent.projection, intent.postconditions) ||
1284
+ typeof intent.capabilityEvidenceDigest !== 'string' ||
1285
+ typeof intent.metadataEvidenceDigest !== 'string' ||
1286
+ typeof intent.metadataSemanticsDigest !== 'string' ||
1287
+ !isRecord(intent.metadataContract) ||
1288
+ !Array.isArray(intent.requiredSemanticCapabilities) ||
1289
+ !isRecord(intent.outboundSafety) ||
1290
+ !isRecord(intent.outboundSafetyEvidence) ||
1291
+ !isRecord(intent.executionEvidence) ||
1292
+ typeof intent.previewDigest !== 'string' ||
1293
+ intent.approvalDigest !== intent.previewDigest ||
1294
+ typeof intent.actionDigest !== 'string') {
1295
+ return false;
1296
+ }
1297
+ const operation = action.operation;
1298
+ const projection = intent.projection;
1299
+ const safety = intent.outboundSafetyEvidence;
1300
+ try {
1301
+ normalizeJiraMutationFieldMask({
1302
+ operation,
1303
+ context: action.context,
1304
+ hostCapability: {},
1305
+ normalizedMetadata: {},
1306
+ bindingId: intent.bindingId,
1307
+ fieldMask: intent.fieldMask,
1308
+ projection,
1309
+ outboundSafety: safety,
1310
+ });
1311
+ requireCurrentOutboundSafety(projection, safety);
1312
+ }
1313
+ catch {
1314
+ return false;
1315
+ }
1316
+ if (!semanticValuesEqual(intent.outboundSafety, {
1317
+ projectionDigest: safety.projectionDigest,
1318
+ resultDigest: safety.resultDigest,
1319
+ })) {
1320
+ return false;
1321
+ }
1322
+ if (operation === 'create') {
1323
+ if (intent.stableId !== null ||
1324
+ !isRecord(intent.provenance) ||
1325
+ intent.provenance.bindingId !== intent.bindingId ||
1326
+ typeof intent.provenance.origin !== 'string' ||
1327
+ !intent.provenance.origin) {
1328
+ return false;
1329
+ }
1330
+ }
1331
+ else if (intent.provenance !== null ||
1332
+ typeof intent.stableId !== 'string' ||
1333
+ !validJiraStableIdForContext(intent.stableId, action.context)) {
1334
+ return false;
1335
+ }
1336
+ const fieldMask = intent.fieldMask;
1337
+ const requiredSemanticCapabilities = fieldMask.includes('description')
1338
+ ? ['structural-description-write']
1339
+ : [];
1340
+ if (!semanticValuesEqual(intent.requiredSemanticCapabilities, requiredSemanticCapabilities)) {
1341
+ return false;
1342
+ }
1343
+ let metadataContract;
1344
+ try {
1345
+ if (!hasExactKeys(intent.metadataContract, ['writableFields', 'transitions'])) {
1346
+ return false;
1347
+ }
1348
+ metadataContract = normalizeJiraMetadataContract({
1349
+ evidenceDigest: intent.metadataEvidenceDigest,
1350
+ writableFields: intent.metadataContract
1351
+ .writableFields,
1352
+ transitions: intent.metadataContract.transitions,
1353
+ });
1354
+ }
1355
+ catch {
1356
+ return false;
1357
+ }
1358
+ if (!semanticValuesEqual(metadataContract, intent.metadataContract) ||
1359
+ semanticDigest(metadataContract) !== intent.metadataSemanticsDigest) {
1360
+ return false;
1361
+ }
1362
+ const baseExecutionEvidence = {
1363
+ capabilityEvidenceDigest: intent.capabilityEvidenceDigest,
1364
+ capabilityObservedAt: intent.executionEvidence.capabilityObservedAt,
1365
+ metadataEvidenceDigest: intent.metadataEvidenceDigest,
1366
+ metadataSemanticsDigest: intent.metadataSemanticsDigest,
1367
+ projectionDigest: safety.projectionDigest,
1368
+ outboundSafetyResultDigest: safety.resultDigest,
1369
+ descriptionAdfEvidence: intent.executionEvidence.descriptionAdfEvidence,
1370
+ };
1371
+ if (!hasExactKeys(intent.executionEvidence, [
1372
+ 'capabilityEvidenceDigest',
1373
+ 'capabilityObservedAt',
1374
+ 'metadataEvidenceDigest',
1375
+ 'metadataSemanticsDigest',
1376
+ 'projectionDigest',
1377
+ 'outboundSafetyResultDigest',
1378
+ 'descriptionAdfEvidence',
1379
+ 'previewDigest',
1380
+ 'approvalDigest',
1381
+ 'actionDigest',
1382
+ ]) ||
1383
+ !Number.isFinite(Date.parse(String(baseExecutionEvidence.capabilityObservedAt))) ||
1384
+ !validDescriptionAdfEvidence(baseExecutionEvidence.descriptionAdfEvidence, fieldMask.includes('description')) ||
1385
+ !validJiraStructuralDescriptionWrite(intent.structuralDescriptionWrite, intent.bindingId, intent.projection, baseExecutionEvidence.descriptionAdfEvidence, fieldMask.includes('description'))) {
1386
+ return false;
1387
+ }
1388
+ const expectedPreviewDigest = semanticDigest({
1389
+ provider: 'jira',
1390
+ operation,
1391
+ context: action.context,
1392
+ bindingId: intent.bindingId,
1393
+ stableId: intent.stableId,
1394
+ provenance: intent.provenance,
1395
+ fieldMask,
1396
+ projection,
1397
+ postconditions: projection,
1398
+ metadataContract,
1399
+ structuralDescriptionWrite: intent.structuralDescriptionWrite,
1400
+ requiredSemanticCapabilities,
1401
+ executionEvidence: baseExecutionEvidence,
1402
+ });
1403
+ const { actionDigest: _actionDigest, executionEvidence: _executionEvidence, ...baseIntent } = intent;
1404
+ const expectedActionDigest = semanticDigest({
1405
+ provider: 'jira',
1406
+ operation,
1407
+ context: action.context,
1408
+ intent: baseIntent,
1409
+ executionEvidence: baseExecutionEvidence,
1410
+ });
1411
+ return (intent.previewDigest === expectedPreviewDigest &&
1412
+ intent.actionDigest === expectedActionDigest &&
1413
+ semanticValuesEqual(intent.executionEvidence, {
1414
+ ...baseExecutionEvidence,
1415
+ previewDigest: expectedPreviewDigest,
1416
+ approvalDigest: expectedPreviewDigest,
1417
+ actionDigest: expectedActionDigest,
1418
+ }) &&
1419
+ semanticValuesEqual(intent.readbackContract, {
1420
+ pinned: true,
1421
+ requireStableIdentity: true,
1422
+ requireExactContext: true,
1423
+ fields: fieldMask,
1424
+ }));
1425
+ }
1426
+ function validJiraDuplicateAction(action) {
1427
+ if (action.provider !== 'jira' ||
1428
+ action.operation !== 'search-duplicates' ||
1429
+ !hasPinnedJiraContext(action.context) ||
1430
+ !hasExactKeys(action.intent, [
1431
+ 'query',
1432
+ 'queryDigest',
1433
+ 'capabilityEvidenceDigest',
1434
+ 'capabilityObservedAt',
1435
+ 'resultContract',
1436
+ 'actionDigest',
1437
+ ]) ||
1438
+ !isRecord(action.intent.query) ||
1439
+ !isRecord(action.intent.resultContract) ||
1440
+ typeof action.intent.capabilityEvidenceDigest !== 'string' ||
1441
+ typeof action.intent.capabilityObservedAt !== 'string' ||
1442
+ !Number.isFinite(Date.parse(action.intent.capabilityObservedAt)) ||
1443
+ !validJiraActionDigest(action)) {
1444
+ return false;
1445
+ }
1446
+ const query = action.intent.query;
1447
+ const historicalKeys = query.historicalKeys;
1448
+ const maxResults = Number(action.intent.resultContract.maxResults);
1449
+ if (typeof query.provenanceToken !== 'string' ||
1450
+ !query.provenanceToken ||
1451
+ Buffer.byteLength(query.provenanceToken, 'utf8') > 512 ||
1452
+ typeof query.reservedBindingId !== 'string' ||
1453
+ !query.reservedBindingId ||
1454
+ Buffer.byteLength(query.reservedBindingId, 'utf8') > 128 ||
1455
+ !Array.isArray(historicalKeys) ||
1456
+ historicalKeys.length > 64 ||
1457
+ new Set(historicalKeys).size !== historicalKeys.length ||
1458
+ historicalKeys.some((key) => typeof key !== 'string' || !ISSUE_KEY.test(key)) ||
1459
+ query.siteId !== action.context.siteId ||
1460
+ query.projectId !== action.context.projectId ||
1461
+ !Number.isInteger(maxResults) ||
1462
+ maxResults < 1 ||
1463
+ maxResults > 100) {
1464
+ return false;
1465
+ }
1466
+ const expectedQuery = {
1467
+ provenanceToken: query.provenanceToken,
1468
+ reservedBindingId: query.reservedBindingId,
1469
+ historicalKeys,
1470
+ siteId: action.context.siteId,
1471
+ projectId: action.context.projectId,
1472
+ };
1473
+ return (semanticValuesEqual(query, expectedQuery) &&
1474
+ action.intent.queryDigest === semanticDigest(expectedQuery) &&
1475
+ semanticValuesEqual(action.intent.resultContract, {
1476
+ maxResults,
1477
+ classifications: ['no-match', 'one-match', 'ambiguous'],
1478
+ requireStableIssueId: true,
1479
+ requireExactContext: true,
1480
+ matchStatus: 'evidence-until-identity-and-context-verified',
1481
+ }));
1482
+ }
1483
+ function validJiraActionDigest(action) {
1484
+ if (action.provider !== 'jira' || !hasPinnedJiraContext(action.context))
1485
+ return false;
1486
+ const { actionDigest, ...intent } = action.intent;
1487
+ return (typeof actionDigest === 'string' &&
1488
+ actionDigest ===
1489
+ semanticDigest({
1490
+ provider: 'jira',
1491
+ operation: action.operation,
1492
+ context: action.context,
1493
+ intent,
1494
+ }));
1495
+ }
1496
+ function semanticValuesEqual(left, right) {
1497
+ try {
1498
+ return semanticDigest(left) === semanticDigest(right);
1499
+ }
1500
+ catch {
1501
+ return false;
1502
+ }
1503
+ }
1504
+ function hasExactKeys(value, expected) {
1505
+ const actual = Object.keys(value).sort();
1506
+ return semanticValuesEqual(actual, [...expected].sort());
1507
+ }
1508
+ function validDescriptionAdfEvidence(value, required) {
1509
+ if (!required)
1510
+ return value === null;
1511
+ return (isRecord(value) &&
1512
+ hasExactKeys(value, [
1513
+ 'bindingId',
1514
+ 'beforeDocumentDigest',
1515
+ 'afterDocumentDigest',
1516
+ 'surroundingDigest',
1517
+ 'managedTextDigest',
1518
+ ]) &&
1519
+ Object.values(value).every((entry) => typeof entry === 'string' && entry.length > 0));
1520
+ }
1521
+ function validJiraStructuralDescriptionWrite(value, bindingId, projection, evidence, required) {
1522
+ if (!required)
1523
+ return value === null && evidence === null;
1524
+ if (!isRecord(value) ||
1525
+ !isRecord(evidence) ||
1526
+ !hasExactKeys(value, [
1527
+ 'kind',
1528
+ 'bindingId',
1529
+ 'document',
1530
+ 'documentDigest',
1531
+ 'beforeDocumentDigest',
1532
+ 'surroundingDigest',
1533
+ 'managedTextDigest',
1534
+ ]) ||
1535
+ value.kind !== 'replace-managed-description' ||
1536
+ value.bindingId !== bindingId ||
1537
+ value.bindingId !== evidence.bindingId ||
1538
+ typeof projection.description !== 'string') {
1539
+ return false;
1540
+ }
1541
+ try {
1542
+ validateJiraAdfDocument(value.document);
1543
+ const inspection = inspectJiraAdf(value.document, bindingId);
1544
+ return (inspection.state === 'managed' &&
1545
+ inspection.managedText === projection.description &&
1546
+ semanticDigest(value.document) === value.documentDigest &&
1547
+ value.documentDigest === evidence.afterDocumentDigest &&
1548
+ value.beforeDocumentDigest === evidence.beforeDocumentDigest &&
1549
+ value.surroundingDigest === evidence.surroundingDigest &&
1550
+ value.managedTextDigest === evidence.managedTextDigest &&
1551
+ value.surroundingDigest === inspection.surroundingDigest &&
1552
+ value.managedTextDigest === semanticDigest(projection.description));
1553
+ }
1554
+ catch {
1555
+ return false;
1556
+ }
1557
+ }
1558
+ function verifyJiraDescriptionReadback(action, issue) {
1559
+ const executionEvidence = action.intent.executionEvidence;
1560
+ if (!isRecord(executionEvidence))
1561
+ return false;
1562
+ const evidence = executionEvidence.descriptionAdfEvidence;
1563
+ if (!validDescriptionAdfEvidence(evidence, true) || evidence === null) {
1564
+ return false;
1565
+ }
1566
+ const document = issue.extensions.descriptionAdf;
1567
+ try {
1568
+ validateJiraAdfDocument(document);
1569
+ const inspection = inspectJiraAdf(document, evidence.bindingId);
1570
+ return (inspection.documentDigest === evidence.afterDocumentDigest &&
1571
+ inspection.surroundingDigest === evidence.surroundingDigest &&
1572
+ inspection.managedText !== null &&
1573
+ semanticDigest(inspection.managedText) === evidence.managedTextDigest);
1574
+ }
1575
+ catch {
1576
+ return false;
1577
+ }
1578
+ }
1579
+ function unavailableJiraRead(classification, reasons) {
1580
+ return { classification, issue: null, preservePriorEvidence: true, reasons };
1581
+ }
1582
+ function parseJiraHostCapability(value) {
1583
+ if (!isRecord(value) ||
1584
+ value.provider !== 'jira' ||
1585
+ !isRecord(value.context) ||
1586
+ !Array.isArray(value.operations) ||
1587
+ !Array.isArray(value.observableFields) ||
1588
+ !Array.isArray(value.semanticCapabilities) ||
1589
+ typeof value.accountId !== 'string' ||
1590
+ typeof value.siteId !== 'string' ||
1591
+ typeof value.projectId !== 'string' ||
1592
+ typeof value.evidenceDigest !== 'string' ||
1593
+ typeof value.observedAt !== 'string') {
1594
+ return null;
1595
+ }
1596
+ return value;
1597
+ }
1598
+ function isRecord(value) {
1599
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
1600
+ }
1601
+ function assertJiraObservation(observation) {
1602
+ if (observation.provider !== 'jira')
1603
+ throw new Error('Expected a Jira observation.');
1604
+ if (!observation.capabilityEvidenceDigest)
1605
+ throw new Error('Jira capability evidence is missing.');
1606
+ }
1607
+ function requiredContext(observation, key) {
1608
+ const value = observation.context[key];
1609
+ if (typeof value !== 'string' || value.length === 0) {
1610
+ throw new Error(`Jira observation context '${key}' is missing.`);
1611
+ }
1612
+ return value;
1613
+ }
1614
+ function requiredString(fields, key) {
1615
+ const value = fields[key];
1616
+ if (typeof value !== 'string' || value.length === 0) {
1617
+ throw new Error(`Jira observation field '${key}' is invalid.`);
1618
+ }
1619
+ return value;
1620
+ }
1621
+ function nullableString(value) {
1622
+ if (value === null || value === undefined)
1623
+ return null;
1624
+ if (typeof value !== 'string')
1625
+ throw new Error('Jira optional text field is invalid.');
1626
+ return value;
1627
+ }
1628
+ function stringArray(value) {
1629
+ if (value === undefined)
1630
+ return [];
1631
+ if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
1632
+ throw new Error('Jira observation alias list is invalid.');
1633
+ }
1634
+ return uniqueStrings(value);
1635
+ }
1636
+ function uniqueStrings(values) {
1637
+ return [...new Set(values.filter(Boolean))];
1638
+ }
1639
+ function suppressInboundText(value, field, suppressedFields) {
1640
+ if (!containsSensitiveContentSignal(value))
1641
+ return value;
1642
+ suppressedFields.push(field);
1643
+ return WHOLE_FIELD_SUPPRESSION_MARKER;
1644
+ }
1645
+ function suppressNullableInboundText(value, field, suppressedFields) {
1646
+ const normalized = nullableString(value);
1647
+ return normalized === null
1648
+ ? null
1649
+ : suppressInboundText(normalized, field, suppressedFields);
1650
+ }