@claudian-collab/protocol 4.2.0 → 4.3.0

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/README.md CHANGED
@@ -20,7 +20,7 @@ or compatibility rules.
20
20
  - Shared limits that client and server must agree on (`COLLAB_LIMITS`)
21
21
  - Protocol-version negotiation (`COLLAB_PROTOCOL_VERSION`, envelope decoding)
22
22
  - Canonical Project checkpoint profiles, logical records, authority-generation transfer, exact membership claims, target-signed redemption receipts, offline backup operational continuity, and Project retirement contracts
23
- - Cloud binding v5 route construction/matching, bounded checkpoint streams, capability negotiation, strict success/error envelopes, private-development bootstrap, Project snapshot, and redacted event contracts
23
+ - Cloud binding v6 route construction/matching, bounded checkpoint streams, capability negotiation, strict success/error envelopes, private-development bootstrap, Project snapshot, and redacted event contracts
24
24
  - Markdown-derived Ticket-reference and Member-mention semantics shared by
25
25
  client rendering and authority derivation
26
26
 
@@ -37,7 +37,7 @@ fixed part of the detail so the whole detail stays within
37
37
  has one compatible final-serialization limit. Request comments and Ticket
38
38
  accepted relations also have shared authority-enforced total limits; summary
39
39
  counts let complete consumers reject partial or cross-snapshot assembly.
40
- Cloud binding v5 retains the authority-neutral Project snapshot, nine redacted durable event kinds plus `snapshot.required`, eighteen capability tokens, the exact ordinary Project/Git route catalog, bounded authority-transfer artifact routes, and six private-development bootstrap bindings. Wire v9 retains target-confirmed Cloud-to-LAN invalidation and cleanup to the existing authority-transfer contract. Its domain-separated Ed25519 proof binds the exact target Member, source and target generations, checkpoint, stage, current claim batch when present, and cleanup digest; Manager cancellation, timeout, disconnect, or target absence does not prove cleanup. The shared checkpoint container is exactly `checkpoint.json`, `coordination.ndjson`, and `repository.bundle`; authority-transfer and export use wire-visible coordination format v1, and offline backup uses format v3. Portable profiles exclude engine storage, paths, credentials, tokens, private keys, and operational refs, and backups exclude plaintext invitation secrets, raw claims, and private keys. Existing LAN Join, invitation, endpoint, membership lifecycle, Host-transfer, snapshot, event, and HTTP bindings remain independently application-owned; the dedicated LAN authority-transfer transport binds shared payloads without creating another shared operation registry.
40
+ Cloud binding v6 retains the authority-neutral Project snapshot, nine redacted durable event kinds plus `snapshot.required`, eighteen capability tokens, the exact ordinary Project/Git route catalog, bounded authority-transfer artifact routes, and six private-development bootstrap bindings. Wire v10 adds direct Project-scoped Ticket-number lookup and retains target-confirmed Cloud-to-LAN invalidation and cleanup to the existing authority-transfer contract. Its domain-separated Ed25519 proof binds the exact target Member, source and target generations, checkpoint, stage, current claim batch when present, and cleanup digest; Manager cancellation, timeout, disconnect, or target absence does not prove cleanup. The shared checkpoint container is exactly `checkpoint.json`, `coordination.ndjson`, and `repository.bundle`; authority-transfer and export use wire-visible coordination format v1, and offline backup uses format v3. Portable profiles exclude engine storage, paths, credentials, tokens, private keys, and operational refs, and backups exclude plaintext invitation secrets, raw claims, and private keys. Existing LAN Join, invitation, endpoint, membership lifecycle, Host-transfer, snapshot, event, and HTTP bindings remain independently application-owned; the dedicated LAN authority-transfer transport binds shared payloads without creating another shared operation registry.
41
41
 
42
42
  Backup coordination artifacts are limited to 256 MiB of actual UTF-8 bytes, including the final newline. The decoder, encoder, and consistency checker apply this same inclusive limit without rewriting principals into an intermediate format. Valid current-format artifacts at the limit are accepted; an additional byte is rejected.
43
43
 
@@ -64,9 +64,9 @@ part of the public surface.
64
64
 
65
65
  Package SemVer, canonical wire version, and Cloud binding version are independent concepts.
66
66
 
67
- - **Package version** (this `package.json`): `4.2.0`. Behavior-preserving implementation refactors and reviewed compatible defect corrections may use a patch release when public declarations, runtime exports, and accepted wire/binding semantics remain unchanged. Minor releases may add backward-compatible API. Removing or incompatibly changing an existing declaration, export, runtime behavior, codec, error, limit, ref rule, operation, or compatibility rule requires a major release. Package API SemVer is classified independently from the wire and Cloud binding contracts, but a compatible package addition cannot exempt a changed wire contract from a wire-version increase or a changed Cloud binding contract from a binding-version increase. The package version never signals wire compatibility by itself.
68
- - **Wire version** (`COLLAB_PROTOCOL_VERSION`): currently `9`. The supported range is exactly `[9, 9]`. This is independent from Cloud binding version `5` and the application-owned LAN bindings. Any change to an existing envelope, DTO, operation payload shape, or operation definition requires a new wire-protocol version. This includes additive operations: every wire-contract change increases the canonical wire version.
69
- - **Cloud binding version** (`COLLAB_CLOUD_BINDING_VERSION`): currently `5`. It versions Cloud routes, bounded transfer streams, and capabilities independently from package and wire versions. Every Cloud binding-contract change increases this version. Bindings before v5 and wire versions before v9 are unsupported rather than translated or dual-interpreted.
67
+ - **Package version** (this `package.json`): `4.3.0`. Behavior-preserving implementation refactors and reviewed compatible defect corrections may use a patch release when public declarations, runtime exports, and accepted wire/binding semantics remain unchanged. Minor releases may add backward-compatible API. Removing or incompatibly changing an existing declaration, export, runtime behavior, codec, error, limit, ref rule, operation, or compatibility rule requires a major release. Package API SemVer is classified independently from the wire and Cloud binding contracts, but a compatible package addition cannot exempt a changed wire contract from a wire-version increase or a changed Cloud binding contract from a binding-version increase. The package version never signals wire compatibility by itself.
68
+ - **Wire version** (`COLLAB_PROTOCOL_VERSION`): currently `10`. The supported range is exactly `[10, 10]`. This is independent from Cloud binding version `6` and the application-owned LAN bindings. Any change to an existing envelope, DTO, operation payload shape, or operation definition requires a new wire-protocol version. This includes additive operations: every wire-contract change increases the canonical wire version.
69
+ - **Cloud binding version** (`COLLAB_CLOUD_BINDING_VERSION`): currently `6`. It versions Cloud routes, bounded transfer streams, and capabilities independently from package and wire versions. Every Cloud binding-contract change increases this version. Bindings before v6 and wire versions before v10 are unsupported rather than translated or dual-interpreted.
70
70
 
71
71
  ### Compatibility behavior
72
72
 
@@ -2,7 +2,7 @@ import { COLLAB_PROTOCOL_VERSION } from './CollabConstants';
2
2
  import { CollabError, type CollabDiagnosticContext, type CollabErrorCode, type CollabRecoveryAction } from './CollabError';
3
3
  import { COLLAB_PROJECT_CHECKPOINT_ARTIFACTS } from './CollabProjectCheckpoint';
4
4
  import type { CollabIsoTimestamp, CollabProjectId } from './types';
5
- export declare const COLLAB_CLOUD_BINDING_VERSION: 5;
5
+ export declare const COLLAB_CLOUD_BINDING_VERSION: 6;
6
6
  export declare const COLLAB_CLOUD_CAPABILITY_DOCUMENT_SCHEMA_VERSION: 2;
7
7
  export declare const COLLAB_CLOUD_CAPABILITIES: readonly ["accept", "authority-transfer", "cloud-imported-membership-claims", "cloud-project-create", "cloud-project-invitations", "cloud-project-join", "cloud-project-leave", "cloud-project-manager-responsibility", "cloud-project-membership", "development-bootstrap", "git-receive-pack-personal-ref", "git-upload-pack", "project-checkpoint-export", "project-events", "project-retirement", "project-snapshot", "requests", "tickets"];
8
8
  export type CollabCloudCapability = typeof COLLAB_CLOUD_CAPABILITIES[number];
@@ -23,7 +23,7 @@ const CollabControlOperationCodecs_1 = require("./CollabControlOperationCodecs")
23
23
  const CollabError_1 = require("./CollabError");
24
24
  const CollabValidation_1 = require("./CollabValidation");
25
25
  const CollabProjectCheckpoint_1 = require("./CollabProjectCheckpoint");
26
- exports.COLLAB_CLOUD_BINDING_VERSION = 5;
26
+ exports.COLLAB_CLOUD_BINDING_VERSION = 6;
27
27
  exports.COLLAB_CLOUD_CAPABILITY_DOCUMENT_SCHEMA_VERSION = 2;
28
28
  exports.COLLAB_CLOUD_CAPABILITIES = Object.freeze([
29
29
  'accept',
@@ -188,7 +188,7 @@ function collabCloudProjectOperationRoute(projectId, operation) {
188
188
  assertProjectId(projectId);
189
189
  if (!isCloudJsonOperation(operation))
190
190
  invalidRoute();
191
- return route('POST', `/v5/projects/${projectId}/operations/${operation}`, {
191
+ return route('POST', `/v6/projects/${projectId}/operations/${operation}`, {
192
192
  kind: 'project-operation',
193
193
  operation,
194
194
  projectId,
@@ -198,7 +198,7 @@ function collabCloudProjectEventsRoute(projectId, afterSequence) {
198
198
  assertProjectId(projectId);
199
199
  if (!Number.isSafeInteger(afterSequence) || afterSequence < 0)
200
200
  invalidRoute();
201
- return route('GET', `/v5/projects/${projectId}/events?afterSequence=${afterSequence}`, {
201
+ return route('GET', `/v6/projects/${projectId}/events?afterSequence=${afterSequence}`, {
202
202
  afterSequence,
203
203
  kind: 'project-events',
204
204
  projectId,
@@ -209,11 +209,11 @@ function collabCloudGitRoute(projectId, routeKind, service) {
209
209
  if (routeKind === 'info-refs') {
210
210
  if (service !== 'git-upload-pack' && service !== 'git-receive-pack')
211
211
  invalidRoute();
212
- return route('GET', `/v5/projects/${projectId}/repository.git/info/refs?service=${service}`, { kind: 'git-info-refs', projectId, service });
212
+ return route('GET', `/v6/projects/${projectId}/repository.git/info/refs?service=${service}`, { kind: 'git-info-refs', projectId, service });
213
213
  }
214
214
  if (service !== undefined)
215
215
  invalidRoute();
216
- return route('POST', `/v5/projects/${projectId}/repository.git/${routeKind}`, {
216
+ return route('POST', `/v6/projects/${projectId}/repository.git/${routeKind}`, {
217
217
  kind: routeKind,
218
218
  projectId,
219
219
  });
@@ -224,7 +224,7 @@ function collabCloudAuthorityTransferArtifactRoute(projectId, transferId, direct
224
224
  if (direction !== 'download' && direction !== 'upload'
225
225
  || !COLLAB_PROJECT_CHECKPOINT_ARTIFACTS_SET.has(artifact))
226
226
  invalidRoute();
227
- return route(direction === 'upload' ? 'PUT' : 'GET', `/v5/projects/${projectId}/authority-transfers/${transferId}/checkpoint/${artifact}`, {
227
+ return route(direction === 'upload' ? 'PUT' : 'GET', `/v6/projects/${projectId}/authority-transfers/${transferId}/checkpoint/${artifact}`, {
228
228
  artifact,
229
229
  direction,
230
230
  kind: 'authority-transfer-artifact',
@@ -237,7 +237,7 @@ function collabCloudProjectCheckpointExportArtifactRoute(projectId, exportId, ar
237
237
  assertAttemptId(exportId);
238
238
  if (!COLLAB_PROJECT_CHECKPOINT_ARTIFACTS_SET.has(artifact))
239
239
  invalidRoute();
240
- return route('GET', `/v5/projects/${projectId}/checkpoint-exports/${exportId}/checkpoint/${artifact}`, {
240
+ return route('GET', `/v6/projects/${projectId}/checkpoint-exports/${exportId}/checkpoint/${artifact}`, {
241
241
  artifact,
242
242
  exportId,
243
243
  kind: 'project-checkpoint-export-artifact',
@@ -249,7 +249,7 @@ function collabCloudProjectCheckpointExportRoute(projectId, exportId, operation)
249
249
  assertAttemptId(exportId);
250
250
  if (operation !== 'begin' && operation !== 'status')
251
251
  invalidRoute();
252
- return route(operation === 'begin' ? 'POST' : 'GET', `/v5/projects/${projectId}/checkpoint-exports/${exportId}`, { exportId, kind: 'project-checkpoint-export', operation, projectId });
252
+ return route(operation === 'begin' ? 'POST' : 'GET', `/v6/projects/${projectId}/checkpoint-exports/${exportId}`, { exportId, kind: 'project-checkpoint-export', operation, projectId });
253
253
  }
254
254
  function decodeCollabCloudProjectCheckpointExportStatus(value) {
255
255
  const source = exactRecord(value, 'checkpointExport', [
@@ -287,7 +287,7 @@ function decodeCollabCloudProjectCheckpointExportStatus(value) {
287
287
  });
288
288
  }
289
289
  function collabDevelopmentBootstrapRoute(operation, attemptId) {
290
- const base = '/v5/development/bootstrap/attempts';
290
+ const base = '/v6/development/bootstrap/attempts';
291
291
  if (operation === 'beginDevelopmentBootstrap') {
292
292
  if (attemptId !== undefined)
293
293
  invalidRoute();
@@ -346,7 +346,7 @@ function matchCollabCloudRoute(method, target) {
346
346
  && url.pathname === '/collab/capabilities'
347
347
  && exactQuery(url, []))
348
348
  return { kind: 'capabilities' };
349
- if (segments[0] === 'v5'
349
+ if (segments[0] === 'v6'
350
350
  && segments[1] === 'projects'
351
351
  && (0, CollabValidation_1.isCollabProjectId)(segments[2])) {
352
352
  const projectId = segments[2];
@@ -425,7 +425,7 @@ function matchCollabCloudRoute(method, target) {
425
425
  && exactQuery(url, []))
426
426
  return { kind: segments[4], projectId };
427
427
  }
428
- if (segments[0] !== 'v5'
428
+ if (segments[0] !== 'v6'
429
429
  || segments[1] !== 'development'
430
430
  || segments[2] !== 'bootstrap'
431
431
  || segments[3] !== 'attempts'
@@ -1,4 +1,4 @@
1
- export declare const COLLAB_PROTOCOL_VERSION: 9;
1
+ export declare const COLLAB_PROTOCOL_VERSION: 10;
2
2
  export declare const COLLAB_MAIN_REF: "refs/heads/main";
3
3
  export declare const COLLAB_MEMBER_REF_PREFIX: "refs/heads/members/";
4
4
  export declare const COLLAB_LIMITS: Readonly<{
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.COLLAB_LIMITS = exports.COLLAB_MEMBER_REF_PREFIX = exports.COLLAB_MAIN_REF = exports.COLLAB_PROTOCOL_VERSION = void 0;
4
- exports.COLLAB_PROTOCOL_VERSION = 9;
4
+ exports.COLLAB_PROTOCOL_VERSION = 10;
5
5
  exports.COLLAB_MAIN_REF = 'refs/heads/main';
6
6
  exports.COLLAB_MEMBER_REF_PREFIX = 'refs/heads/members/';
7
7
  exports.COLLAB_LIMITS = Object.freeze({
@@ -32,6 +32,7 @@ export declare const COLLAB_CONTROL_OPERATION_CODECS: Readonly<{
32
32
  readonly createComment: CollabControlOperationCodec<import("./CollabProtocol").CreateCommentRequest, import("./CollabProtocol").CreateCommentResponse>;
33
33
  readonly listTickets: CollabControlOperationCodec<import("./CollabProtocol").ListTicketsRequest, import("./types").CollabTicketPage>;
34
34
  readonly getTicket: CollabControlOperationCodec<import("./CollabProtocol").GetTicketRequest, import("./types").CollabTicketDetail>;
35
+ readonly resolveTicketNumber: CollabControlOperationCodec<import("./CollabProtocol").ResolveTicketNumberRequest, import("./CollabProtocol").ResolveTicketNumberResponse>;
35
36
  readonly listTicketComments: CollabControlOperationCodec<import("./CollabProtocol").ListTicketCommentsRequest, import("./types").CollabTicketCommentPage>;
36
37
  readonly listTicketAcceptedRelations: CollabControlOperationCodec<import("./CollabProtocol").ListTicketAcceptedRelationsRequest, import("./types").CollabTicketAcceptedRelationPage>;
37
38
  readonly createTicket: CollabControlOperationCodec<import("./CollabProtocol").CreateTicketRequest, import("./CollabProtocol").CreateTicketResponse>;
@@ -38,6 +38,7 @@ function decodeResponse(operation, input) {
38
38
  return (0, CollabProjectRetirement_1.decodeCollabProjectRetirementOperationResponse)(operation, input);
39
39
  }
40
40
  switch (operation) {
41
+ case 'resolveTicketNumber': return (0, CollabRequestTicketResponseCodecs_1.decodeResolveTicketNumberResponse)(input);
41
42
  case 'getRequest': return (0, CollabRequestTicketResponseCodecs_1.decodeRequestDetailResponse)(input);
42
43
  case 'listRequestComments': return (0, CollabRequestTicketResponseCodecs_1.decodeCommentPageResponse)(input);
43
44
  case 'ensureMyRequest': return (0, CollabRequestTicketResponseCodecs_1.decodeEnsureMyRequestResponse)(input);
@@ -69,6 +70,7 @@ exports.COLLAB_CONTROL_OPERATION_CODECS = Object.freeze({
69
70
  createComment: codec('createComment'),
70
71
  listTickets: codec('listTickets'),
71
72
  getTicket: codec('getTicket'),
73
+ resolveTicketNumber: codec('resolveTicketNumber'),
72
74
  listTicketComments: codec('listTicketComments'),
73
75
  listTicketAcceptedRelations: codec('listTicketAcceptedRelations'),
74
76
  createTicket: codec('createTicket'),
@@ -2,7 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseCollabMemberMentions = parseCollabMemberMentions;
4
4
  const CollabMarkdownProse_1 = require("./CollabMarkdownProse");
5
- const MENTION_ADJACENT_CHARACTER = /[\p{L}\p{N}_@-]/u;
5
+ const MENTION_PRECEDING_CHARACTER = /[\p{L}\p{N}_@-]$/u;
6
+ const MENTION_FOLLOWING_CHARACTER = /^[\p{L}\p{N}_@-]/u;
6
7
  function parseCollabMemberMentions(markdown, targets) {
7
8
  const candidates = mentionCandidates(targets);
8
9
  if (candidates.length === 0)
@@ -15,14 +16,15 @@ function parseCollabMemberMentions(markdown, targets) {
15
16
  if (mentionStart < 0)
16
17
  break;
17
18
  searchFrom = mentionStart + 1;
18
- const preceding = prose[mentionStart - 1];
19
- if (preceding !== undefined && MENTION_ADJACENT_CHARACTER.test(preceding))
19
+ const preceding = prose.slice(Math.max(0, mentionStart - 2), mentionStart);
20
+ if (MENTION_PRECEDING_CHARACTER.test(preceding))
20
21
  continue;
21
22
  const candidate = candidates.find(entry => {
22
23
  if (!prose.startsWith(entry.name, mentionStart + 1))
23
24
  return false;
24
- const following = prose[mentionStart + entry.name.length + 1];
25
- return following === undefined || !MENTION_ADJACENT_CHARACTER.test(following);
25
+ const followingOffset = mentionStart + entry.name.length + 1;
26
+ const following = prose.slice(followingOffset, followingOffset + 2);
27
+ return !MENTION_FOLLOWING_CHARACTER.test(following);
26
28
  });
27
29
  if (!candidate)
28
30
  continue;
@@ -1384,6 +1384,13 @@ function validateContinuity(records) {
1384
1384
  const terminalResponders = new Map(records
1385
1385
  .filter((item) => (item.kind === 'terminal-responder'))
1386
1386
  .map(item => [item.value.operationId, item]));
1387
+ const terminalMembers = new Map([...terminalResponders].map(([operationId, terminal]) => [
1388
+ operationId,
1389
+ {
1390
+ acknowledgements: new Map(terminal.value.acknowledgements.map(item => [item.memberId, item])),
1391
+ eligibleMemberIds: new Set(terminal.value.eligibleMemberIds),
1392
+ },
1393
+ ]));
1387
1394
  const terminalPrincipals = new Map(records
1388
1395
  .filter((item) => (item.kind === 'terminal-principal'))
1389
1396
  .map(item => [`${item.value.operationId}:${item.value.memberId}`, item]));
@@ -1400,6 +1407,9 @@ function validateContinuity(records) {
1400
1407
  .filter((item) => (item.kind === 'protected-invitation-envelope'))
1401
1408
  .map(item => [item.value.invitationId, item]));
1402
1409
  const claimOverrides = records.filter((item) => (item.kind === 'transferred-membership-claim-override'));
1410
+ const redeemedOverrideIdentities = new Set(claimOverrides
1411
+ .filter(item => item.value.state === 'redeemed')
1412
+ .map(item => (`${item.value.transferId}:${item.value.memberId}:${item.value.claimSha256}:${item.value.redemptionReceiptId}`)));
1403
1413
  const claimOverrideEnvelopes = new Map(records
1404
1414
  .filter((item) => (item.kind === 'protected-claim-override-envelope'))
1405
1415
  .map(item => [
@@ -1412,6 +1422,28 @@ function validateContinuity(records) {
1412
1422
  .filter((item) => (item.kind === 'project-membership-recovery'))
1413
1423
  .map(item => [item.value.operationId, item]));
1414
1424
  const secretReplayTombstones = records.filter((item) => (item.kind === 'secret-replay-tombstone'));
1425
+ const secretTombstonesByIdentity = new Map(secretReplayTombstones.map(item => [
1426
+ `${item.recordId}:${item.value.requestFingerprint}`,
1427
+ item,
1428
+ ]));
1429
+ const liveSecretIdentities = new Set();
1430
+ for (const invitation of invitations.values()) {
1431
+ if (invitationEnvelopes.has(invitation.value.invitationId)) {
1432
+ liveSecretIdentities.add(`createProjectInvitation:${invitation.value.issuedByMemberId}:${invitation.value.idempotencyKey}`);
1433
+ }
1434
+ }
1435
+ for (const override of claimOverrides) {
1436
+ if (claimOverrideEnvelopes.has(override.recordId)) {
1437
+ liveSecretIdentities.add(`reissueTransferredMembershipClaim:${override.value.managerMemberId}:${override.value.idempotencyKey}`);
1438
+ }
1439
+ }
1440
+ const liveIdempotencyIdentities = new Set(records
1441
+ .filter(item => item.kind === 'idempotency-result')
1442
+ .map(item => `${item.value.operation}:${item.value.memberId}:${item.value.idempotencyKey}`));
1443
+ for (const offer of managerOffers) {
1444
+ liveIdempotencyIdentities.add(`createManagerResponsibilityOffer:${offer.value.sourceManagerMemberId}:${offer.value.idempotencyKey}`);
1445
+ }
1446
+ const claimOverrideRecordIds = new Set(claimOverrides.map(item => item.recordId));
1415
1447
  const nonterminalLifecycles = [...lifecycles.values()].filter(item => (item.value.state === 'active' || item.value.state === 'recovery-required'));
1416
1448
  if (nonterminalLifecycles.length > 1
1417
1449
  || principalBindingRecords.some(item => (memberRecords.get(item.value.memberId)?.value.status !== 'active'))
@@ -1664,13 +1696,8 @@ function validateContinuity(records) {
1664
1696
  const recovery = recoveries.get(claim.value.transferId);
1665
1697
  const member = memberRecords.get(claim.value.memberId);
1666
1698
  const redemptionReceipt = redemptionReceipts.get(`${claim.value.transferId}:${claim.value.memberId}`);
1667
- const redeemedOverride = redemptionReceipt === undefined
1668
- ? undefined
1669
- : claimOverrides.find(item => (item.value.transferId === claim.value.transferId
1670
- && item.value.memberId === claim.value.memberId
1671
- && item.value.state === 'redeemed'
1672
- && item.value.claimSha256 === redemptionReceipt.value.receipt.claimSha256
1673
- && item.value.redemptionReceiptId === redemptionReceipt.value.receipt.receiptId));
1699
+ const hasRedeemedOverride = redemptionReceipt !== undefined
1700
+ && redeemedOverrideIdentities.has(`${claim.value.transferId}:${claim.value.memberId}:${redemptionReceipt.value.receipt.claimSha256}:${redemptionReceipt.value.receipt.receiptId}`);
1674
1701
  if (member?.value.status !== 'active'
1675
1702
  || recovery === undefined
1676
1703
  || Date.parse(member.value.createdAt) > Date.parse(recovery.value.createdAt)
@@ -1684,8 +1711,8 @@ function validateContinuity(records) {
1684
1711
  || (claim.value.state === 'redeemed' && redemptionReceipt === undefined)
1685
1712
  || (redemptionReceipt !== undefined
1686
1713
  && claim.value.state !== 'redeemed'
1687
- && redeemedOverride === undefined)
1688
- || (claim.value.state === 'redeemed' && redeemedOverride !== undefined)) {
1714
+ && !hasRedeemedOverride)
1715
+ || (claim.value.state === 'redeemed' && hasRedeemedOverride)) {
1689
1716
  throw invalidPayload('records');
1690
1717
  }
1691
1718
  }
@@ -1728,10 +1755,7 @@ function validateContinuity(records) {
1728
1755
  }
1729
1756
  for (const invitation of invitations.values()) {
1730
1757
  const envelope = invitationEnvelopes.get(invitation.value.invitationId);
1731
- const tombstone = secretReplayTombstones.find(item => (item.value.operation === 'createProjectInvitation'
1732
- && item.value.actorMemberId === invitation.value.issuedByMemberId
1733
- && item.value.idempotencyKey === invitation.value.idempotencyKey
1734
- && item.value.requestFingerprint === invitation.value.requestFingerprint));
1758
+ const tombstone = secretTombstonesByIdentity.get(`createProjectInvitation:${invitation.value.issuedByMemberId}:${invitation.value.idempotencyKey}:${invitation.value.requestFingerprint}`);
1735
1759
  if (!members.has(invitation.value.issuedByMemberId)
1736
1760
  || (envelope === undefined) === (tombstone === undefined)
1737
1761
  || (tombstone !== undefined
@@ -1752,10 +1776,7 @@ function validateContinuity(records) {
1752
1776
  values.push(override);
1753
1777
  overridesByMember.set(identity, values);
1754
1778
  const envelope = claimOverrideEnvelopes.get(`${identity}:${override.value.claimGeneration}`);
1755
- const tombstone = secretReplayTombstones.find(item => (item.value.operation === 'reissueTransferredMembershipClaim'
1756
- && item.value.actorMemberId === override.value.managerMemberId
1757
- && item.value.idempotencyKey === override.value.idempotencyKey
1758
- && item.value.requestFingerprint === override.value.requestFingerprint));
1779
+ const tombstone = secretTombstonesByIdentity.get(`reissueTransferredMembershipClaim:${override.value.managerMemberId}:${override.value.idempotencyKey}:${override.value.requestFingerprint}`);
1759
1780
  if (!members.has(override.value.memberId)
1760
1781
  || !members.has(override.value.managerMemberId)
1761
1782
  || (envelope === undefined) === (tombstone === undefined)
@@ -1810,9 +1831,7 @@ function validateContinuity(records) {
1810
1831
  throw invalidPayload('records');
1811
1832
  }
1812
1833
  for (const envelope of claimOverrideEnvelopes.values()) {
1813
- if (!claimOverrides.some(item => (item.value.transferId === envelope.value.transferId
1814
- && item.value.memberId === envelope.value.memberId
1815
- && item.value.claimGeneration === envelope.value.claimGeneration)))
1834
+ if (!claimOverrideRecordIds.has(envelope.recordId))
1816
1835
  throw invalidPayload('records');
1817
1836
  }
1818
1837
  const currentOffers = managerOffers.filter(item => (item.value.state === 'offered' || item.value.state === 'acknowledged'));
@@ -1825,26 +1844,14 @@ function validateContinuity(records) {
1825
1844
  !== currentOffers.length)
1826
1845
  throw invalidPayload('records');
1827
1846
  for (const tombstone of secretReplayTombstones) {
1828
- const liveSecretRecord = tombstone.value.operation === 'createProjectInvitation'
1829
- ? [...invitations.values()].some(item => (item.value.issuedByMemberId === tombstone.value.actorMemberId
1830
- && item.value.idempotencyKey === tombstone.value.idempotencyKey
1831
- && invitationEnvelopes.has(item.value.invitationId)))
1832
- : claimOverrides.some(item => (item.value.managerMemberId === tombstone.value.actorMemberId
1833
- && item.value.idempotencyKey === tombstone.value.idempotencyKey
1834
- && claimOverrideEnvelopes.has(`${item.value.transferId}:${item.value.memberId}:${item.value.claimGeneration}`)));
1835
- if (!members.has(tombstone.value.actorMemberId) || liveSecretRecord) {
1847
+ if (!members.has(tombstone.value.actorMemberId)
1848
+ || liveSecretIdentities.has(tombstone.recordId)) {
1836
1849
  throw invalidPayload('records');
1837
1850
  }
1838
1851
  }
1839
1852
  for (const tombstone of membershipIdempotencyTombstones) {
1840
- const exactResult = records.some(item => (item.kind === 'idempotency-result'
1841
- && item.value.memberId === tombstone.value.actorMemberId
1842
- && item.value.operation === tombstone.value.operation
1843
- && item.value.idempotencyKey === tombstone.value.idempotencyKey));
1844
- const exactOffer = tombstone.value.operation === 'createManagerResponsibilityOffer'
1845
- && managerOffers.some(item => (item.value.sourceManagerMemberId === tombstone.value.actorMemberId
1846
- && item.value.idempotencyKey === tombstone.value.idempotencyKey));
1847
- if (!members.has(tombstone.value.actorMemberId) || exactResult || exactOffer) {
1853
+ if (!members.has(tombstone.value.actorMemberId)
1854
+ || liveIdempotencyIdentities.has(tombstone.recordId)) {
1848
1855
  throw invalidPayload('records');
1849
1856
  }
1850
1857
  }
@@ -1901,18 +1908,14 @@ function validateContinuity(records) {
1901
1908
  const lifecycle = lifecycles.get(receipt.transferId);
1902
1909
  const terminalPrincipal = terminalPrincipals.get(identity);
1903
1910
  const lanToCloud = recovery?.value.sourceAuthority.kind === 'lan';
1904
- const redeemedOverride = claimOverrides.find(item => (item.value.transferId === receipt.transferId
1905
- && item.value.memberId === receipt.memberId
1906
- && item.value.state === 'redeemed'
1907
- && item.value.claimSha256 === receipt.claimSha256
1908
- && item.value.redemptionReceiptId === receipt.receiptId));
1911
+ const hasRedeemedOverride = redeemedOverrideIdentities.has(`${receipt.transferId}:${receipt.memberId}:${receipt.claimSha256}:${receipt.receiptId}`);
1909
1912
  const matchesSourceClaim = claim?.value.state === 'redeemed'
1910
1913
  && claim.value.claimSha256 === receipt.claimSha256
1911
1914
  && claim.value.checkpointSha256 === receipt.checkpointSha256
1912
1915
  && claim.value.operationIntentId === receipt.operationIntentId
1913
1916
  && claim.value.redemptionReceiptId === receipt.receiptId;
1914
1917
  const matchesOverride = claim !== undefined
1915
- && redeemedOverride !== undefined
1918
+ && hasRedeemedOverride
1916
1919
  && claim.value.checkpointSha256 === receipt.checkpointSha256;
1917
1920
  if (recovery === undefined
1918
1921
  || lifecycle === undefined
@@ -1932,7 +1935,8 @@ function validateContinuity(records) {
1932
1935
  }
1933
1936
  if (item.kind === 'terminal-principal') {
1934
1937
  const terminal = terminalResponders.get(item.value.operationId);
1935
- const acknowledgement = terminal?.value.acknowledgements.find(value => (value.memberId === item.value.memberId));
1938
+ const terminalMemberIndex = terminalMembers.get(item.value.operationId);
1939
+ const acknowledgement = terminalMemberIndex?.acknowledgements.get(item.value.memberId);
1936
1940
  const expectedOperationKind = terminal?.value.operation === 'getProjectAuthorityTransfer'
1937
1941
  ? 'authority-transfer'
1938
1942
  : terminal?.value.operation === 'retireProject'
@@ -1941,7 +1945,7 @@ function validateContinuity(records) {
1941
1945
  if (!members.has(item.value.memberId)
1942
1946
  || terminal === undefined
1943
1947
  || expectedOperationKind !== item.value.operationKind
1944
- || !terminal.value.eligibleMemberIds.includes(item.value.memberId)
1948
+ || !terminalMemberIndex?.eligibleMemberIds.has(item.value.memberId)
1945
1949
  || (item.value.acknowledgedAt === null) !== (acknowledgement === undefined)
1946
1950
  || (acknowledgement !== undefined && (acknowledgement.acknowledgedAt !== item.value.acknowledgedAt
1947
1951
  || acknowledgement.principalId !== item.value.principalId))
@@ -2097,8 +2101,8 @@ function decodeCollabProjectBackupCheckpointCoordinationNdjson(value) {
2097
2101
  }
2098
2102
  function encodeCollabProjectBackupCheckpointCoordinationNdjson(records) {
2099
2103
  const value = records.map(item => JSON.stringify(item)).join('\n') + '\n';
2100
- return decodeCollabProjectBackupCheckpointCoordinationNdjson(value)
2101
- .map(item => JSON.stringify(item)).join('\n') + '\n';
2104
+ decodeCollabProjectBackupCheckpointCoordinationNdjson(value);
2105
+ return value;
2102
2106
  }
2103
2107
  function validateCollabProjectBackupCheckpointConsistency(manifest, records) {
2104
2108
  const decodedManifest = decodeCollabProjectBackupCheckpointManifest(manifest);
@@ -939,9 +939,10 @@ function terminalResponderRecord(source, recordId, revision, deferPrincipals) {
939
939
  : token(acknowledgement, 'principalId'),
940
940
  };
941
941
  });
942
+ const eligibleMemberIdSet = new Set(eligibleMemberIds);
942
943
  const acknowledgementPrincipals = new Set();
943
944
  acknowledgements.forEach((item, index) => {
944
- if (!eligibleMemberIds.includes(item.memberId)
945
+ if (!eligibleMemberIdSet.has(item.memberId)
945
946
  || (index > 0 && acknowledgements[index - 1].memberId.localeCompare(item.memberId, 'en-US') >= 0)
946
947
  || (!deferPrincipals && acknowledgementPrincipals.has(item.principalId)))
947
948
  throw invalidPayload('acknowledgements');
@@ -1313,8 +1314,8 @@ function decodeCollabProjectCheckpointCoordinationNdjson(value, profile) {
1313
1314
  }
1314
1315
  function encodeCollabProjectCheckpointCoordinationNdjson(records, profile) {
1315
1316
  const encoded = records.map(record => JSON.stringify(record)).join('\n') + '\n';
1316
- return decodeCollabProjectCheckpointCoordinationNdjson(encoded, profile)
1317
- .map(record => JSON.stringify(record)).join('\n') + '\n';
1317
+ decodeCollabProjectCheckpointCoordinationNdjson(encoded, profile);
1318
+ return encoded;
1318
1319
  }
1319
1320
  function checkpointRecordGitOids(checkpointRecord) {
1320
1321
  switch (checkpointRecord.kind) {
@@ -74,6 +74,13 @@ export interface GetTicketRequest {
74
74
  projectId: CollabProjectId;
75
75
  ticketId: CollabTicketId;
76
76
  }
77
+ export interface ResolveTicketNumberRequest {
78
+ projectId: CollabProjectId;
79
+ ticketNumber: number;
80
+ }
81
+ export interface ResolveTicketNumberResponse {
82
+ ticketId: CollabTicketId | null;
83
+ }
77
84
  export interface CreateTicketRequest extends CollabMutationContext {
78
85
  title: string;
79
86
  body: string;
@@ -134,6 +141,7 @@ export interface CollabControlOperationMap extends CollabAuthorityTransferOperat
134
141
  createComment: CollabControlOperationDefinition<CreateCommentRequest, CreateCommentResponse>;
135
142
  listTickets: CollabControlOperationDefinition<ListTicketsRequest, CollabTicketPage>;
136
143
  getTicket: CollabControlOperationDefinition<GetTicketRequest, CollabTicketDetail>;
144
+ resolveTicketNumber: CollabControlOperationDefinition<ResolveTicketNumberRequest, ResolveTicketNumberResponse>;
137
145
  listTicketComments: CollabControlOperationDefinition<ListTicketCommentsRequest, CollabTicketCommentPage>;
138
146
  listTicketAcceptedRelations: CollabControlOperationDefinition<ListTicketAcceptedRelationsRequest, CollabTicketAcceptedRelationPage>;
139
147
  createTicket: CollabControlOperationDefinition<CreateTicketRequest, CreateTicketResponse>;
@@ -1,5 +1,6 @@
1
- import type { CollabControlOperationMap, CollabDecodeResult } from './CollabProtocol';
2
- export type CollabRequestTicketOperation = 'acceptRequest' | 'closeTicket' | 'createComment' | 'createTicket' | 'createTicketComment' | 'ensureMyRequest' | 'getRequest' | 'getTicket' | 'listRequestComments' | 'listTicketAcceptedRelations' | 'listTicketComments' | 'listTickets' | 'reopenTicket' | 'updateMyRequestMetadata' | 'updateTicketContent';
1
+ import type { CollabControlOperationMap, CollabDecodeResult, ResolveTicketNumberRequest } from './CollabProtocol';
2
+ export type CollabRequestTicketOperation = 'acceptRequest' | 'closeTicket' | 'createComment' | 'createTicket' | 'createTicketComment' | 'ensureMyRequest' | 'getRequest' | 'getTicket' | 'listRequestComments' | 'listTicketAcceptedRelations' | 'listTicketComments' | 'listTickets' | 'reopenTicket' | 'resolveTicketNumber' | 'updateMyRequestMetadata' | 'updateTicketContent';
3
3
  type OperationRequest<Operation extends CollabRequestTicketOperation> = CollabControlOperationMap[Operation]['request'];
4
+ export declare function decodeResolveTicketNumber(input: unknown): ResolveTicketNumberRequest | null;
4
5
  export declare function decodeCollabRequestTicketOperationRequest<Operation extends CollabRequestTicketOperation>(operation: Operation, input: unknown): CollabDecodeResult<OperationRequest<Operation>>;
5
6
  export {};
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.decodeResolveTicketNumber = decodeResolveTicketNumber;
3
4
  exports.decodeCollabRequestTicketOperationRequest = decodeCollabRequestTicketOperationRequest;
4
5
  const CollabConstants_1 = require("./CollabConstants");
5
6
  const CollabError_1 = require("./CollabError");
@@ -53,10 +54,19 @@ function resolvingTickets(value) {
53
54
  }
54
55
  return result;
55
56
  }
57
+ function decodeResolveTicketNumber(input) {
58
+ return isRecord(input)
59
+ && hasExactKeys(input, ['projectId', 'ticketNumber'])
60
+ && (0, CollabValidation_1.isCollabProjectId)(input.projectId)
61
+ && isRevision(input.ticketNumber, 1)
62
+ ? { projectId: input.projectId, ticketNumber: input.ticketNumber }
63
+ : null;
64
+ }
56
65
  function decodeRequestTicketRequest(operation, input) {
57
66
  if (!isRecord(input))
58
67
  return null;
59
68
  switch (operation) {
69
+ case 'resolveTicketNumber': return decodeResolveTicketNumber(input);
60
70
  case 'getRequest':
61
71
  return (0, CollabValidation_1.isCollabProjectId)(input.projectId) && (0, CollabValidation_1.isCollabOpaqueId)(input.requestId)
62
72
  ? { projectId: input.projectId, requestId: input.requestId }
@@ -255,6 +265,7 @@ const INVALID_REASONS = {
255
265
  listTicketComments: 'ticket-comment-page-query-invalid',
256
266
  listTickets: 'ticket-list-query-invalid',
257
267
  reopenTicket: 'ticket-mutation-payload-invalid',
268
+ resolveTicketNumber: 'ticket-number-query-invalid',
258
269
  updateMyRequestMetadata: 'request-metadata-payload-invalid',
259
270
  updateTicketContent: 'ticket-content-payload-invalid',
260
271
  };
@@ -1,4 +1,4 @@
1
- import { type AcceptResponse, type CreateCommentResponse, type CreateTicketCommentResponse, type CreateTicketResponse, type EnsureMyRequestResponse, type TicketMutationResponse, type UpdateMyRequestMetadataResponse } from './CollabProtocol';
1
+ import { type AcceptResponse, type CreateCommentResponse, type CreateTicketCommentResponse, type CreateTicketResponse, type EnsureMyRequestResponse, type ResolveTicketNumberResponse, type TicketMutationResponse, type UpdateMyRequestMetadataResponse } from './CollabProtocol';
2
2
  import { type CollabCommentPage, type CollabRequestDetail, type CollabTicketAcceptedRelationPage, type CollabTicketCommentPage, type CollabTicketDetail, type CollabTicketPage } from './types';
3
3
  export declare function decodeEnsureMyRequestResponse(value: unknown): EnsureMyRequestResponse;
4
4
  export declare function decodeRequestDetailResponse(value: unknown): CollabRequestDetail;
@@ -8,6 +8,7 @@ export declare function decodeTicketAcceptedRelationPageResponse(value: unknown)
8
8
  export declare function decodeCreateCommentResponse(value: unknown): CreateCommentResponse;
9
9
  export declare function decodeAcceptResponse(value: unknown): AcceptResponse;
10
10
  export declare function decodeTicketPageResponse(value: unknown): CollabTicketPage;
11
+ export declare function decodeResolveTicketNumberResponse(value: unknown): ResolveTicketNumberResponse;
11
12
  export declare function decodeTicketDetailResponse(value: unknown): CollabTicketDetail;
12
13
  export declare function decodeCreateTicketResponse(value: unknown): CreateTicketResponse;
13
14
  export declare function decodeTicketMutationResponse(value: unknown): TicketMutationResponse;
@@ -8,6 +8,7 @@ exports.decodeTicketAcceptedRelationPageResponse = decodeTicketAcceptedRelationP
8
8
  exports.decodeCreateCommentResponse = decodeCreateCommentResponse;
9
9
  exports.decodeAcceptResponse = decodeAcceptResponse;
10
10
  exports.decodeTicketPageResponse = decodeTicketPageResponse;
11
+ exports.decodeResolveTicketNumberResponse = decodeResolveTicketNumberResponse;
11
12
  exports.decodeTicketDetailResponse = decodeTicketDetailResponse;
12
13
  exports.decodeCreateTicketResponse = decodeCreateTicketResponse;
13
14
  exports.decodeTicketMutationResponse = decodeTicketMutationResponse;
@@ -345,6 +346,15 @@ function decodeTicketPageResponse(value) {
345
346
  tickets: data.tickets.map(ticketSummary),
346
347
  };
347
348
  }
349
+ function decodeResolveTicketNumberResponse(value) {
350
+ const data = record(envelopeData(value), 'data');
351
+ if (!Object.hasOwn(data, 'ticketId') || Object.keys(data).length !== 1) {
352
+ throw decodeError('ticketNumberResolution');
353
+ }
354
+ return {
355
+ ticketId: data.ticketId === null ? null : string(data, 'ticketId', 128, CollabValidation_1.isCollabOpaqueId),
356
+ };
357
+ }
348
358
  function decodeTicketDetailResponse(value) {
349
359
  return ticketDetail(envelopeData(value));
350
360
  }
@@ -4,11 +4,26 @@ exports.parseCollabTicketReferences = parseCollabTicketReferences;
4
4
  exports.scanCollabTicketReferences = scanCollabTicketReferences;
5
5
  const CollabConstants_1 = require("./CollabConstants");
6
6
  const CollabMarkdownProse_1 = require("./CollabMarkdownProse");
7
- const CLOSING_KEYWORD_PATTERN = /(?:^|[^A-Za-z])(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)[ \t]*:?[ \t]*$/i;
7
+ const CLOSING_KEYWORD_PATTERN = /^(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)$/i;
8
8
  const TICKET_REFERENCE_PATTERN = /(^|[^#0-9A-Za-z_])#([1-9][0-9]*)(?![#0-9A-Za-z_])/gm;
9
9
  function relationKindBefore(maskedDescription, referenceOffset) {
10
- const prefix = maskedDescription.slice(0, referenceOffset);
11
- return CLOSING_KEYWORD_PATTERN.test(prefix) ? 'resolves' : 'references';
10
+ let wordEnd = referenceOffset;
11
+ while (maskedDescription[wordEnd - 1] === ' ' || maskedDescription[wordEnd - 1] === '\t') {
12
+ wordEnd -= 1;
13
+ }
14
+ if (maskedDescription[wordEnd - 1] === ':') {
15
+ wordEnd -= 1;
16
+ while (maskedDescription[wordEnd - 1] === ' ' || maskedDescription[wordEnd - 1] === '\t') {
17
+ wordEnd -= 1;
18
+ }
19
+ }
20
+ let wordStart = wordEnd;
21
+ while (wordStart > 0 && /[A-Za-z]/.test(maskedDescription[wordStart - 1])) {
22
+ wordStart -= 1;
23
+ }
24
+ return CLOSING_KEYWORD_PATTERN.test(maskedDescription.slice(wordStart, wordEnd))
25
+ ? 'resolves'
26
+ : 'references';
12
27
  }
13
28
  function parseCollabTicketReferences(description) {
14
29
  if (new TextEncoder().encode(description).byteLength >
@@ -3,7 +3,7 @@ import { COLLAB_CONTROL_OPERATION_CODECS, } from './CollabControlOperationCodecs
3
3
  import { COLLAB_ERROR_CODES, CollabError, sanitizeCollabDiagnosticContext, } from './CollabError.mjs';
4
4
  import { isCollabOpaqueId, isCollabProjectId, } from './CollabValidation.mjs';
5
5
  import { COLLAB_CHECKPOINT_ARTIFACT_LIMITS, COLLAB_PROJECT_CHECKPOINT_ARTIFACTS, } from './CollabProjectCheckpoint.mjs';
6
- export const COLLAB_CLOUD_BINDING_VERSION = 5;
6
+ export const COLLAB_CLOUD_BINDING_VERSION = 6;
7
7
  export const COLLAB_CLOUD_CAPABILITY_DOCUMENT_SCHEMA_VERSION = 2;
8
8
  export const COLLAB_CLOUD_CAPABILITIES = Object.freeze([
9
9
  'accept',
@@ -168,7 +168,7 @@ export function collabCloudProjectOperationRoute(projectId, operation) {
168
168
  assertProjectId(projectId);
169
169
  if (!isCloudJsonOperation(operation))
170
170
  invalidRoute();
171
- return route('POST', `/v5/projects/${projectId}/operations/${operation}`, {
171
+ return route('POST', `/v6/projects/${projectId}/operations/${operation}`, {
172
172
  kind: 'project-operation',
173
173
  operation,
174
174
  projectId,
@@ -178,7 +178,7 @@ export function collabCloudProjectEventsRoute(projectId, afterSequence) {
178
178
  assertProjectId(projectId);
179
179
  if (!Number.isSafeInteger(afterSequence) || afterSequence < 0)
180
180
  invalidRoute();
181
- return route('GET', `/v5/projects/${projectId}/events?afterSequence=${afterSequence}`, {
181
+ return route('GET', `/v6/projects/${projectId}/events?afterSequence=${afterSequence}`, {
182
182
  afterSequence,
183
183
  kind: 'project-events',
184
184
  projectId,
@@ -189,11 +189,11 @@ export function collabCloudGitRoute(projectId, routeKind, service) {
189
189
  if (routeKind === 'info-refs') {
190
190
  if (service !== 'git-upload-pack' && service !== 'git-receive-pack')
191
191
  invalidRoute();
192
- return route('GET', `/v5/projects/${projectId}/repository.git/info/refs?service=${service}`, { kind: 'git-info-refs', projectId, service });
192
+ return route('GET', `/v6/projects/${projectId}/repository.git/info/refs?service=${service}`, { kind: 'git-info-refs', projectId, service });
193
193
  }
194
194
  if (service !== undefined)
195
195
  invalidRoute();
196
- return route('POST', `/v5/projects/${projectId}/repository.git/${routeKind}`, {
196
+ return route('POST', `/v6/projects/${projectId}/repository.git/${routeKind}`, {
197
197
  kind: routeKind,
198
198
  projectId,
199
199
  });
@@ -204,7 +204,7 @@ export function collabCloudAuthorityTransferArtifactRoute(projectId, transferId,
204
204
  if (direction !== 'download' && direction !== 'upload'
205
205
  || !COLLAB_PROJECT_CHECKPOINT_ARTIFACTS_SET.has(artifact))
206
206
  invalidRoute();
207
- return route(direction === 'upload' ? 'PUT' : 'GET', `/v5/projects/${projectId}/authority-transfers/${transferId}/checkpoint/${artifact}`, {
207
+ return route(direction === 'upload' ? 'PUT' : 'GET', `/v6/projects/${projectId}/authority-transfers/${transferId}/checkpoint/${artifact}`, {
208
208
  artifact,
209
209
  direction,
210
210
  kind: 'authority-transfer-artifact',
@@ -217,7 +217,7 @@ export function collabCloudProjectCheckpointExportArtifactRoute(projectId, expor
217
217
  assertAttemptId(exportId);
218
218
  if (!COLLAB_PROJECT_CHECKPOINT_ARTIFACTS_SET.has(artifact))
219
219
  invalidRoute();
220
- return route('GET', `/v5/projects/${projectId}/checkpoint-exports/${exportId}/checkpoint/${artifact}`, {
220
+ return route('GET', `/v6/projects/${projectId}/checkpoint-exports/${exportId}/checkpoint/${artifact}`, {
221
221
  artifact,
222
222
  exportId,
223
223
  kind: 'project-checkpoint-export-artifact',
@@ -229,7 +229,7 @@ export function collabCloudProjectCheckpointExportRoute(projectId, exportId, ope
229
229
  assertAttemptId(exportId);
230
230
  if (operation !== 'begin' && operation !== 'status')
231
231
  invalidRoute();
232
- return route(operation === 'begin' ? 'POST' : 'GET', `/v5/projects/${projectId}/checkpoint-exports/${exportId}`, { exportId, kind: 'project-checkpoint-export', operation, projectId });
232
+ return route(operation === 'begin' ? 'POST' : 'GET', `/v6/projects/${projectId}/checkpoint-exports/${exportId}`, { exportId, kind: 'project-checkpoint-export', operation, projectId });
233
233
  }
234
234
  export function decodeCollabCloudProjectCheckpointExportStatus(value) {
235
235
  const source = exactRecord(value, 'checkpointExport', [
@@ -267,7 +267,7 @@ export function decodeCollabCloudProjectCheckpointExportStatus(value) {
267
267
  });
268
268
  }
269
269
  export function collabDevelopmentBootstrapRoute(operation, attemptId) {
270
- const base = '/v5/development/bootstrap/attempts';
270
+ const base = '/v6/development/bootstrap/attempts';
271
271
  if (operation === 'beginDevelopmentBootstrap') {
272
272
  if (attemptId !== undefined)
273
273
  invalidRoute();
@@ -326,7 +326,7 @@ export function matchCollabCloudRoute(method, target) {
326
326
  && url.pathname === '/collab/capabilities'
327
327
  && exactQuery(url, []))
328
328
  return { kind: 'capabilities' };
329
- if (segments[0] === 'v5'
329
+ if (segments[0] === 'v6'
330
330
  && segments[1] === 'projects'
331
331
  && isCollabProjectId(segments[2])) {
332
332
  const projectId = segments[2];
@@ -405,7 +405,7 @@ export function matchCollabCloudRoute(method, target) {
405
405
  && exactQuery(url, []))
406
406
  return { kind: segments[4], projectId };
407
407
  }
408
- if (segments[0] !== 'v5'
408
+ if (segments[0] !== 'v6'
409
409
  || segments[1] !== 'development'
410
410
  || segments[2] !== 'bootstrap'
411
411
  || segments[3] !== 'attempts'
@@ -1,4 +1,4 @@
1
- export const COLLAB_PROTOCOL_VERSION = 9;
1
+ export const COLLAB_PROTOCOL_VERSION = 10;
2
2
  export const COLLAB_MAIN_REF = 'refs/heads/main';
3
3
  export const COLLAB_MEMBER_REF_PREFIX = 'refs/heads/members/';
4
4
  export const COLLAB_LIMITS = Object.freeze({
@@ -3,7 +3,7 @@ import { CollabError } from './CollabError.mjs';
3
3
  import { COLLAB_PROJECT_MEMBERSHIP_OPERATION_CODECS } from './CollabProjectMembership.mjs';
4
4
  import { COLLAB_PROJECT_RETIREMENT_OPERATIONS, decodeCollabProjectRetirementOperationRequest, decodeCollabProjectRetirementOperationResponse, } from './CollabProjectRetirement.mjs';
5
5
  import { decodeCollabRequestTicketOperationRequest, } from './CollabRequestTicketRequestCodecs.mjs';
6
- import { decodeAcceptResponse, decodeCommentPageResponse, decodeCreateCommentResponse, decodeCreateTicketResponse, decodeEnsureMyRequestResponse, decodeRequestDetailResponse, decodeTicketAcceptedRelationPageResponse, decodeTicketCommentPageResponse, decodeTicketCommentResponse, decodeTicketDetailResponse, decodeTicketMutationResponse, decodeTicketPageResponse, decodeUpdateRequestMetadataResponse, } from './CollabRequestTicketResponseCodecs.mjs';
6
+ import { decodeAcceptResponse, decodeCommentPageResponse, decodeCreateCommentResponse, decodeCreateTicketResponse, decodeEnsureMyRequestResponse, decodeRequestDetailResponse, decodeResolveTicketNumberResponse, decodeTicketAcceptedRelationPageResponse, decodeTicketCommentPageResponse, decodeTicketCommentResponse, decodeTicketDetailResponse, decodeTicketMutationResponse, decodeTicketPageResponse, decodeUpdateRequestMetadataResponse, } from './CollabRequestTicketResponseCodecs.mjs';
7
7
  const AUTHORITY_TRANSFER_OPERATION_SET = new Set(COLLAB_AUTHORITY_TRANSFER_OPERATIONS);
8
8
  const PROJECT_RETIREMENT_OPERATION_SET = new Set(COLLAB_PROJECT_RETIREMENT_OPERATIONS);
9
9
  function lifecycleDecodeResult(decode) {
@@ -34,6 +34,7 @@ function decodeResponse(operation, input) {
34
34
  return decodeCollabProjectRetirementOperationResponse(operation, input);
35
35
  }
36
36
  switch (operation) {
37
+ case 'resolveTicketNumber': return decodeResolveTicketNumberResponse(input);
37
38
  case 'getRequest': return decodeRequestDetailResponse(input);
38
39
  case 'listRequestComments': return decodeCommentPageResponse(input);
39
40
  case 'ensureMyRequest': return decodeEnsureMyRequestResponse(input);
@@ -65,6 +66,7 @@ export const COLLAB_CONTROL_OPERATION_CODECS = Object.freeze({
65
66
  createComment: codec('createComment'),
66
67
  listTickets: codec('listTickets'),
67
68
  getTicket: codec('getTicket'),
69
+ resolveTicketNumber: codec('resolveTicketNumber'),
68
70
  listTicketComments: codec('listTicketComments'),
69
71
  listTicketAcceptedRelations: codec('listTicketAcceptedRelations'),
70
72
  createTicket: codec('createTicket'),
@@ -1,5 +1,6 @@
1
1
  import { maskCollabMarkdownProse } from './CollabMarkdownProse.mjs';
2
- const MENTION_ADJACENT_CHARACTER = /[\p{L}\p{N}_@-]/u;
2
+ const MENTION_PRECEDING_CHARACTER = /[\p{L}\p{N}_@-]$/u;
3
+ const MENTION_FOLLOWING_CHARACTER = /^[\p{L}\p{N}_@-]/u;
3
4
  export function parseCollabMemberMentions(markdown, targets) {
4
5
  const candidates = mentionCandidates(targets);
5
6
  if (candidates.length === 0)
@@ -12,14 +13,15 @@ export function parseCollabMemberMentions(markdown, targets) {
12
13
  if (mentionStart < 0)
13
14
  break;
14
15
  searchFrom = mentionStart + 1;
15
- const preceding = prose[mentionStart - 1];
16
- if (preceding !== undefined && MENTION_ADJACENT_CHARACTER.test(preceding))
16
+ const preceding = prose.slice(Math.max(0, mentionStart - 2), mentionStart);
17
+ if (MENTION_PRECEDING_CHARACTER.test(preceding))
17
18
  continue;
18
19
  const candidate = candidates.find(entry => {
19
20
  if (!prose.startsWith(entry.name, mentionStart + 1))
20
21
  return false;
21
- const following = prose[mentionStart + entry.name.length + 1];
22
- return following === undefined || !MENTION_ADJACENT_CHARACTER.test(following);
22
+ const followingOffset = mentionStart + entry.name.length + 1;
23
+ const following = prose.slice(followingOffset, followingOffset + 2);
24
+ return !MENTION_FOLLOWING_CHARACTER.test(following);
23
25
  });
24
26
  if (!candidate)
25
27
  continue;
@@ -1374,6 +1374,13 @@ function validateContinuity(records) {
1374
1374
  const terminalResponders = new Map(records
1375
1375
  .filter((item) => (item.kind === 'terminal-responder'))
1376
1376
  .map(item => [item.value.operationId, item]));
1377
+ const terminalMembers = new Map([...terminalResponders].map(([operationId, terminal]) => [
1378
+ operationId,
1379
+ {
1380
+ acknowledgements: new Map(terminal.value.acknowledgements.map(item => [item.memberId, item])),
1381
+ eligibleMemberIds: new Set(terminal.value.eligibleMemberIds),
1382
+ },
1383
+ ]));
1377
1384
  const terminalPrincipals = new Map(records
1378
1385
  .filter((item) => (item.kind === 'terminal-principal'))
1379
1386
  .map(item => [`${item.value.operationId}:${item.value.memberId}`, item]));
@@ -1390,6 +1397,9 @@ function validateContinuity(records) {
1390
1397
  .filter((item) => (item.kind === 'protected-invitation-envelope'))
1391
1398
  .map(item => [item.value.invitationId, item]));
1392
1399
  const claimOverrides = records.filter((item) => (item.kind === 'transferred-membership-claim-override'));
1400
+ const redeemedOverrideIdentities = new Set(claimOverrides
1401
+ .filter(item => item.value.state === 'redeemed')
1402
+ .map(item => (`${item.value.transferId}:${item.value.memberId}:${item.value.claimSha256}:${item.value.redemptionReceiptId}`)));
1393
1403
  const claimOverrideEnvelopes = new Map(records
1394
1404
  .filter((item) => (item.kind === 'protected-claim-override-envelope'))
1395
1405
  .map(item => [
@@ -1402,6 +1412,28 @@ function validateContinuity(records) {
1402
1412
  .filter((item) => (item.kind === 'project-membership-recovery'))
1403
1413
  .map(item => [item.value.operationId, item]));
1404
1414
  const secretReplayTombstones = records.filter((item) => (item.kind === 'secret-replay-tombstone'));
1415
+ const secretTombstonesByIdentity = new Map(secretReplayTombstones.map(item => [
1416
+ `${item.recordId}:${item.value.requestFingerprint}`,
1417
+ item,
1418
+ ]));
1419
+ const liveSecretIdentities = new Set();
1420
+ for (const invitation of invitations.values()) {
1421
+ if (invitationEnvelopes.has(invitation.value.invitationId)) {
1422
+ liveSecretIdentities.add(`createProjectInvitation:${invitation.value.issuedByMemberId}:${invitation.value.idempotencyKey}`);
1423
+ }
1424
+ }
1425
+ for (const override of claimOverrides) {
1426
+ if (claimOverrideEnvelopes.has(override.recordId)) {
1427
+ liveSecretIdentities.add(`reissueTransferredMembershipClaim:${override.value.managerMemberId}:${override.value.idempotencyKey}`);
1428
+ }
1429
+ }
1430
+ const liveIdempotencyIdentities = new Set(records
1431
+ .filter(item => item.kind === 'idempotency-result')
1432
+ .map(item => `${item.value.operation}:${item.value.memberId}:${item.value.idempotencyKey}`));
1433
+ for (const offer of managerOffers) {
1434
+ liveIdempotencyIdentities.add(`createManagerResponsibilityOffer:${offer.value.sourceManagerMemberId}:${offer.value.idempotencyKey}`);
1435
+ }
1436
+ const claimOverrideRecordIds = new Set(claimOverrides.map(item => item.recordId));
1405
1437
  const nonterminalLifecycles = [...lifecycles.values()].filter(item => (item.value.state === 'active' || item.value.state === 'recovery-required'));
1406
1438
  if (nonterminalLifecycles.length > 1
1407
1439
  || principalBindingRecords.some(item => (memberRecords.get(item.value.memberId)?.value.status !== 'active'))
@@ -1654,13 +1686,8 @@ function validateContinuity(records) {
1654
1686
  const recovery = recoveries.get(claim.value.transferId);
1655
1687
  const member = memberRecords.get(claim.value.memberId);
1656
1688
  const redemptionReceipt = redemptionReceipts.get(`${claim.value.transferId}:${claim.value.memberId}`);
1657
- const redeemedOverride = redemptionReceipt === undefined
1658
- ? undefined
1659
- : claimOverrides.find(item => (item.value.transferId === claim.value.transferId
1660
- && item.value.memberId === claim.value.memberId
1661
- && item.value.state === 'redeemed'
1662
- && item.value.claimSha256 === redemptionReceipt.value.receipt.claimSha256
1663
- && item.value.redemptionReceiptId === redemptionReceipt.value.receipt.receiptId));
1689
+ const hasRedeemedOverride = redemptionReceipt !== undefined
1690
+ && redeemedOverrideIdentities.has(`${claim.value.transferId}:${claim.value.memberId}:${redemptionReceipt.value.receipt.claimSha256}:${redemptionReceipt.value.receipt.receiptId}`);
1664
1691
  if (member?.value.status !== 'active'
1665
1692
  || recovery === undefined
1666
1693
  || Date.parse(member.value.createdAt) > Date.parse(recovery.value.createdAt)
@@ -1674,8 +1701,8 @@ function validateContinuity(records) {
1674
1701
  || (claim.value.state === 'redeemed' && redemptionReceipt === undefined)
1675
1702
  || (redemptionReceipt !== undefined
1676
1703
  && claim.value.state !== 'redeemed'
1677
- && redeemedOverride === undefined)
1678
- || (claim.value.state === 'redeemed' && redeemedOverride !== undefined)) {
1704
+ && !hasRedeemedOverride)
1705
+ || (claim.value.state === 'redeemed' && hasRedeemedOverride)) {
1679
1706
  throw invalidPayload('records');
1680
1707
  }
1681
1708
  }
@@ -1718,10 +1745,7 @@ function validateContinuity(records) {
1718
1745
  }
1719
1746
  for (const invitation of invitations.values()) {
1720
1747
  const envelope = invitationEnvelopes.get(invitation.value.invitationId);
1721
- const tombstone = secretReplayTombstones.find(item => (item.value.operation === 'createProjectInvitation'
1722
- && item.value.actorMemberId === invitation.value.issuedByMemberId
1723
- && item.value.idempotencyKey === invitation.value.idempotencyKey
1724
- && item.value.requestFingerprint === invitation.value.requestFingerprint));
1748
+ const tombstone = secretTombstonesByIdentity.get(`createProjectInvitation:${invitation.value.issuedByMemberId}:${invitation.value.idempotencyKey}:${invitation.value.requestFingerprint}`);
1725
1749
  if (!members.has(invitation.value.issuedByMemberId)
1726
1750
  || (envelope === undefined) === (tombstone === undefined)
1727
1751
  || (tombstone !== undefined
@@ -1742,10 +1766,7 @@ function validateContinuity(records) {
1742
1766
  values.push(override);
1743
1767
  overridesByMember.set(identity, values);
1744
1768
  const envelope = claimOverrideEnvelopes.get(`${identity}:${override.value.claimGeneration}`);
1745
- const tombstone = secretReplayTombstones.find(item => (item.value.operation === 'reissueTransferredMembershipClaim'
1746
- && item.value.actorMemberId === override.value.managerMemberId
1747
- && item.value.idempotencyKey === override.value.idempotencyKey
1748
- && item.value.requestFingerprint === override.value.requestFingerprint));
1769
+ const tombstone = secretTombstonesByIdentity.get(`reissueTransferredMembershipClaim:${override.value.managerMemberId}:${override.value.idempotencyKey}:${override.value.requestFingerprint}`);
1749
1770
  if (!members.has(override.value.memberId)
1750
1771
  || !members.has(override.value.managerMemberId)
1751
1772
  || (envelope === undefined) === (tombstone === undefined)
@@ -1800,9 +1821,7 @@ function validateContinuity(records) {
1800
1821
  throw invalidPayload('records');
1801
1822
  }
1802
1823
  for (const envelope of claimOverrideEnvelopes.values()) {
1803
- if (!claimOverrides.some(item => (item.value.transferId === envelope.value.transferId
1804
- && item.value.memberId === envelope.value.memberId
1805
- && item.value.claimGeneration === envelope.value.claimGeneration)))
1824
+ if (!claimOverrideRecordIds.has(envelope.recordId))
1806
1825
  throw invalidPayload('records');
1807
1826
  }
1808
1827
  const currentOffers = managerOffers.filter(item => (item.value.state === 'offered' || item.value.state === 'acknowledged'));
@@ -1815,26 +1834,14 @@ function validateContinuity(records) {
1815
1834
  !== currentOffers.length)
1816
1835
  throw invalidPayload('records');
1817
1836
  for (const tombstone of secretReplayTombstones) {
1818
- const liveSecretRecord = tombstone.value.operation === 'createProjectInvitation'
1819
- ? [...invitations.values()].some(item => (item.value.issuedByMemberId === tombstone.value.actorMemberId
1820
- && item.value.idempotencyKey === tombstone.value.idempotencyKey
1821
- && invitationEnvelopes.has(item.value.invitationId)))
1822
- : claimOverrides.some(item => (item.value.managerMemberId === tombstone.value.actorMemberId
1823
- && item.value.idempotencyKey === tombstone.value.idempotencyKey
1824
- && claimOverrideEnvelopes.has(`${item.value.transferId}:${item.value.memberId}:${item.value.claimGeneration}`)));
1825
- if (!members.has(tombstone.value.actorMemberId) || liveSecretRecord) {
1837
+ if (!members.has(tombstone.value.actorMemberId)
1838
+ || liveSecretIdentities.has(tombstone.recordId)) {
1826
1839
  throw invalidPayload('records');
1827
1840
  }
1828
1841
  }
1829
1842
  for (const tombstone of membershipIdempotencyTombstones) {
1830
- const exactResult = records.some(item => (item.kind === 'idempotency-result'
1831
- && item.value.memberId === tombstone.value.actorMemberId
1832
- && item.value.operation === tombstone.value.operation
1833
- && item.value.idempotencyKey === tombstone.value.idempotencyKey));
1834
- const exactOffer = tombstone.value.operation === 'createManagerResponsibilityOffer'
1835
- && managerOffers.some(item => (item.value.sourceManagerMemberId === tombstone.value.actorMemberId
1836
- && item.value.idempotencyKey === tombstone.value.idempotencyKey));
1837
- if (!members.has(tombstone.value.actorMemberId) || exactResult || exactOffer) {
1843
+ if (!members.has(tombstone.value.actorMemberId)
1844
+ || liveIdempotencyIdentities.has(tombstone.recordId)) {
1838
1845
  throw invalidPayload('records');
1839
1846
  }
1840
1847
  }
@@ -1891,18 +1898,14 @@ function validateContinuity(records) {
1891
1898
  const lifecycle = lifecycles.get(receipt.transferId);
1892
1899
  const terminalPrincipal = terminalPrincipals.get(identity);
1893
1900
  const lanToCloud = recovery?.value.sourceAuthority.kind === 'lan';
1894
- const redeemedOverride = claimOverrides.find(item => (item.value.transferId === receipt.transferId
1895
- && item.value.memberId === receipt.memberId
1896
- && item.value.state === 'redeemed'
1897
- && item.value.claimSha256 === receipt.claimSha256
1898
- && item.value.redemptionReceiptId === receipt.receiptId));
1901
+ const hasRedeemedOverride = redeemedOverrideIdentities.has(`${receipt.transferId}:${receipt.memberId}:${receipt.claimSha256}:${receipt.receiptId}`);
1899
1902
  const matchesSourceClaim = claim?.value.state === 'redeemed'
1900
1903
  && claim.value.claimSha256 === receipt.claimSha256
1901
1904
  && claim.value.checkpointSha256 === receipt.checkpointSha256
1902
1905
  && claim.value.operationIntentId === receipt.operationIntentId
1903
1906
  && claim.value.redemptionReceiptId === receipt.receiptId;
1904
1907
  const matchesOverride = claim !== undefined
1905
- && redeemedOverride !== undefined
1908
+ && hasRedeemedOverride
1906
1909
  && claim.value.checkpointSha256 === receipt.checkpointSha256;
1907
1910
  if (recovery === undefined
1908
1911
  || lifecycle === undefined
@@ -1922,7 +1925,8 @@ function validateContinuity(records) {
1922
1925
  }
1923
1926
  if (item.kind === 'terminal-principal') {
1924
1927
  const terminal = terminalResponders.get(item.value.operationId);
1925
- const acknowledgement = terminal?.value.acknowledgements.find(value => (value.memberId === item.value.memberId));
1928
+ const terminalMemberIndex = terminalMembers.get(item.value.operationId);
1929
+ const acknowledgement = terminalMemberIndex?.acknowledgements.get(item.value.memberId);
1926
1930
  const expectedOperationKind = terminal?.value.operation === 'getProjectAuthorityTransfer'
1927
1931
  ? 'authority-transfer'
1928
1932
  : terminal?.value.operation === 'retireProject'
@@ -1931,7 +1935,7 @@ function validateContinuity(records) {
1931
1935
  if (!members.has(item.value.memberId)
1932
1936
  || terminal === undefined
1933
1937
  || expectedOperationKind !== item.value.operationKind
1934
- || !terminal.value.eligibleMemberIds.includes(item.value.memberId)
1938
+ || !terminalMemberIndex?.eligibleMemberIds.has(item.value.memberId)
1935
1939
  || (item.value.acknowledgedAt === null) !== (acknowledgement === undefined)
1936
1940
  || (acknowledgement !== undefined && (acknowledgement.acknowledgedAt !== item.value.acknowledgedAt
1937
1941
  || acknowledgement.principalId !== item.value.principalId))
@@ -2087,8 +2091,8 @@ export function decodeCollabProjectBackupCheckpointCoordinationNdjson(value) {
2087
2091
  }
2088
2092
  export function encodeCollabProjectBackupCheckpointCoordinationNdjson(records) {
2089
2093
  const value = records.map(item => JSON.stringify(item)).join('\n') + '\n';
2090
- return decodeCollabProjectBackupCheckpointCoordinationNdjson(value)
2091
- .map(item => JSON.stringify(item)).join('\n') + '\n';
2094
+ decodeCollabProjectBackupCheckpointCoordinationNdjson(value);
2095
+ return value;
2092
2096
  }
2093
2097
  export function validateCollabProjectBackupCheckpointConsistency(manifest, records) {
2094
2098
  const decodedManifest = decodeCollabProjectBackupCheckpointManifest(manifest);
@@ -924,9 +924,10 @@ function terminalResponderRecord(source, recordId, revision, deferPrincipals) {
924
924
  : token(acknowledgement, 'principalId'),
925
925
  };
926
926
  });
927
+ const eligibleMemberIdSet = new Set(eligibleMemberIds);
927
928
  const acknowledgementPrincipals = new Set();
928
929
  acknowledgements.forEach((item, index) => {
929
- if (!eligibleMemberIds.includes(item.memberId)
930
+ if (!eligibleMemberIdSet.has(item.memberId)
930
931
  || (index > 0 && acknowledgements[index - 1].memberId.localeCompare(item.memberId, 'en-US') >= 0)
931
932
  || (!deferPrincipals && acknowledgementPrincipals.has(item.principalId)))
932
933
  throw invalidPayload('acknowledgements');
@@ -1298,8 +1299,8 @@ export function decodeCollabProjectCheckpointCoordinationNdjson(value, profile)
1298
1299
  }
1299
1300
  export function encodeCollabProjectCheckpointCoordinationNdjson(records, profile) {
1300
1301
  const encoded = records.map(record => JSON.stringify(record)).join('\n') + '\n';
1301
- return decodeCollabProjectCheckpointCoordinationNdjson(encoded, profile)
1302
- .map(record => JSON.stringify(record)).join('\n') + '\n';
1302
+ decodeCollabProjectCheckpointCoordinationNdjson(encoded, profile);
1303
+ return encoded;
1303
1304
  }
1304
1305
  function checkpointRecordGitOids(checkpointRecord) {
1305
1306
  switch (checkpointRecord.kind) {
@@ -50,10 +50,19 @@ function resolvingTickets(value) {
50
50
  }
51
51
  return result;
52
52
  }
53
+ export function decodeResolveTicketNumber(input) {
54
+ return isRecord(input)
55
+ && hasExactKeys(input, ['projectId', 'ticketNumber'])
56
+ && isCollabProjectId(input.projectId)
57
+ && isRevision(input.ticketNumber, 1)
58
+ ? { projectId: input.projectId, ticketNumber: input.ticketNumber }
59
+ : null;
60
+ }
53
61
  function decodeRequestTicketRequest(operation, input) {
54
62
  if (!isRecord(input))
55
63
  return null;
56
64
  switch (operation) {
65
+ case 'resolveTicketNumber': return decodeResolveTicketNumber(input);
57
66
  case 'getRequest':
58
67
  return isCollabProjectId(input.projectId) && isCollabOpaqueId(input.requestId)
59
68
  ? { projectId: input.projectId, requestId: input.requestId }
@@ -252,6 +261,7 @@ const INVALID_REASONS = {
252
261
  listTicketComments: 'ticket-comment-page-query-invalid',
253
262
  listTickets: 'ticket-list-query-invalid',
254
263
  reopenTicket: 'ticket-mutation-payload-invalid',
264
+ resolveTicketNumber: 'ticket-number-query-invalid',
255
265
  updateMyRequestMetadata: 'request-metadata-payload-invalid',
256
266
  updateTicketContent: 'ticket-content-payload-invalid',
257
267
  };
@@ -330,6 +330,15 @@ export function decodeTicketPageResponse(value) {
330
330
  tickets: data.tickets.map(ticketSummary),
331
331
  };
332
332
  }
333
+ export function decodeResolveTicketNumberResponse(value) {
334
+ const data = record(envelopeData(value), 'data');
335
+ if (!Object.hasOwn(data, 'ticketId') || Object.keys(data).length !== 1) {
336
+ throw decodeError('ticketNumberResolution');
337
+ }
338
+ return {
339
+ ticketId: data.ticketId === null ? null : string(data, 'ticketId', 128, isCollabOpaqueId),
340
+ };
341
+ }
333
342
  export function decodeTicketDetailResponse(value) {
334
343
  return ticketDetail(envelopeData(value));
335
344
  }
@@ -1,10 +1,25 @@
1
1
  import { COLLAB_LIMITS } from './CollabConstants.mjs';
2
2
  import { maskCollabMarkdownProse } from './CollabMarkdownProse.mjs';
3
- const CLOSING_KEYWORD_PATTERN = /(?:^|[^A-Za-z])(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)[ \t]*:?[ \t]*$/i;
3
+ const CLOSING_KEYWORD_PATTERN = /^(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)$/i;
4
4
  const TICKET_REFERENCE_PATTERN = /(^|[^#0-9A-Za-z_])#([1-9][0-9]*)(?![#0-9A-Za-z_])/gm;
5
5
  function relationKindBefore(maskedDescription, referenceOffset) {
6
- const prefix = maskedDescription.slice(0, referenceOffset);
7
- return CLOSING_KEYWORD_PATTERN.test(prefix) ? 'resolves' : 'references';
6
+ let wordEnd = referenceOffset;
7
+ while (maskedDescription[wordEnd - 1] === ' ' || maskedDescription[wordEnd - 1] === '\t') {
8
+ wordEnd -= 1;
9
+ }
10
+ if (maskedDescription[wordEnd - 1] === ':') {
11
+ wordEnd -= 1;
12
+ while (maskedDescription[wordEnd - 1] === ' ' || maskedDescription[wordEnd - 1] === '\t') {
13
+ wordEnd -= 1;
14
+ }
15
+ }
16
+ let wordStart = wordEnd;
17
+ while (wordStart > 0 && /[A-Za-z]/.test(maskedDescription[wordStart - 1])) {
18
+ wordStart -= 1;
19
+ }
20
+ return CLOSING_KEYWORD_PATTERN.test(maskedDescription.slice(wordStart, wordEnd))
21
+ ? 'resolves'
22
+ : 'references';
8
23
  }
9
24
  export function parseCollabTicketReferences(description) {
10
25
  if (new TextEncoder().encode(description).byteLength >
@@ -13,5 +13,7 @@ export { COLLAB_ERROR_CODES, CollabError, collabErrorGroup, sanitizeCollabDiagno
13
13
  export { parseCollabMemberMentions } from './CollabMemberMentionParser.mjs';
14
14
  export { decodeCollabProtocolEnvelope } from './CollabProtocol.mjs';
15
15
  export { parseCollabTicketReferences, scanCollabTicketReferences, } from './CollabTicketReferenceParser.mjs';
16
+ export { decodeResolveTicketNumber } from './CollabRequestTicketRequestCodecs.mjs';
17
+ export { decodeResolveTicketNumberResponse } from './CollabRequestTicketResponseCodecs.mjs';
16
18
  export { isCollabGitOid, isCollabMemberId, isCollabOpaqueId, isCollabProjectId, } from './CollabValidation.mjs';
17
19
  export { collabMemberRef } from './types.mjs';
package/dist/index.d.ts CHANGED
@@ -25,10 +25,12 @@ export type { CollabDiagnosticContext, CollabDiagnosticValue, CollabErrorCode, C
25
25
  export { parseCollabMemberMentions } from './CollabMemberMentionParser';
26
26
  export type { CollabMemberMentionTarget } from './CollabMemberMentionParser';
27
27
  export { decodeCollabProtocolEnvelope } from './CollabProtocol';
28
- export type { AcceptRequest, AcceptResponse, ChangeTicketStatusRequest, CollabControlOperationDefinition, CollabControlOperationMap, CollabDecodeFailure, CollabDecodeResult, CollabMutationContext, CollabProtocolEnvelope, CreateCommentRequest, CreateCommentResponse, CreateTicketCommentRequest, CreateTicketCommentResponse, CreateTicketRequest, CreateTicketResponse, EnsureMyRequestRequest, EnsureMyRequestResponse, GetRequestRequest, GetTicketRequest, ListRequestCommentsRequest, ListTicketAcceptedRelationsRequest, ListTicketCommentsRequest, ListTicketsRequest, TicketMutationResponse, UpdateMyRequestMetadataRequest, UpdateMyRequestMetadataResponse, UpdateTicketContentRequest, } from './CollabProtocol';
28
+ export type { AcceptRequest, AcceptResponse, ChangeTicketStatusRequest, CollabControlOperationDefinition, CollabControlOperationMap, CollabDecodeFailure, CollabDecodeResult, CollabMutationContext, CollabProtocolEnvelope, CreateCommentRequest, CreateCommentResponse, CreateTicketCommentRequest, CreateTicketCommentResponse, CreateTicketRequest, CreateTicketResponse, EnsureMyRequestRequest, EnsureMyRequestResponse, GetRequestRequest, GetTicketRequest, ListRequestCommentsRequest, ListTicketAcceptedRelationsRequest, ListTicketCommentsRequest, ListTicketsRequest, ResolveTicketNumberRequest, ResolveTicketNumberResponse, TicketMutationResponse, UpdateMyRequestMetadataRequest, UpdateMyRequestMetadataResponse, UpdateTicketContentRequest, } from './CollabProtocol';
29
29
  export { parseCollabTicketReferences, scanCollabTicketReferences, } from './CollabTicketReferenceParser';
30
30
  export type { CollabTicketReferenceParseFailureReason, CollabTicketReferenceParseResult, CollabTicketReferenceScanResult, CollabTicketReferenceToken, } from './CollabTicketReferenceParser';
31
+ export { decodeResolveTicketNumber } from './CollabRequestTicketRequestCodecs';
31
32
  export type { CollabRequestTicketOperation } from './CollabRequestTicketRequestCodecs';
33
+ export { decodeResolveTicketNumberResponse } from './CollabRequestTicketResponseCodecs';
32
34
  export { isCollabGitOid, isCollabMemberId, isCollabOpaqueId, isCollabProjectId, } from './CollabValidation';
33
35
  export { collabMemberRef } from './types';
34
36
  export type { CollabChangedFile, CollabChangeRequest, CollabComment, CollabCommentId, CollabCommentPage, CollabFileChangeKind, CollabGitOid, CollabIdempotencyKey, CollabIsoTimestamp, CollabMember, CollabMemberId, CollabMemberStatus, CollabOperationId, CollabParsedTicketReference, CollabProjectId, CollabRelativePath, CollabRequestDetail, CollabRequestId, CollabRequestStatus, CollabRequestTicketRelation, CollabResolvingTicketExpectation, CollabReviewCondition, CollabRole, CollabTicketAcceptedRelation, CollabTicketAcceptedRelationPage, CollabTicketComment, CollabTicketCommentId, CollabTicketCommentPage, CollabTicketCommitRelationKind, CollabTicketDetail, CollabTicketId, CollabTicketPage, CollabTicketRelationId, CollabTicketStatus, CollabTicketSummary, } from './types';
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.decodeCollabProjectBackupCheckpointManifest = exports.decodeCollabProjectBackupCheckpointCoordinationNdjson = exports.collabProjectBackupIdempotencyRecordId = exports.COLLAB_PROJECT_BACKUP_RECORD_KINDS = exports.COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION = exports.COLLAB_PROJECT_BACKUP_COMPATIBILITY_STAGE = exports.decodeCollabProjectMembershipOperationResponse = exports.decodeCollabProjectMembershipOperationRequest = exports.COLLAB_PROJECT_MEMBERSHIP_OPERATION_CODECS = exports.COLLAB_PROJECT_MEMBERSHIP_OPERATIONS = exports.COLLAB_PROJECT_MEMBERSHIP_LIMITS = exports.validateCollabProjectCheckpointConsistency = exports.encodeCollabProtectedClaimAssociatedData = exports.encodeCollabProjectCheckpointManifestDigestInput = exports.encodeCollabProjectCheckpointManifestCanonicalJson = exports.encodeCollabProjectCheckpointCoordinationNdjson = exports.decodeCollabProjectCheckpointManifest = exports.decodeCollabProjectCheckpointCoordinationNdjson = exports.COLLAB_PROTECTED_CLAIM_ENVELOPE_VERSION = exports.COLLAB_PROTECTED_CLAIM_ENVELOPE_LIMITS = exports.COLLAB_PROJECT_COORDINATION_FORMAT_VERSION = exports.COLLAB_PROJECT_CHECKPOINT_MANIFEST_SCHEMA_VERSION = exports.COLLAB_PROJECT_CHECKPOINT_ARTIFACTS = exports.COLLAB_CHECKPOINT_PROFILES = exports.COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS = exports.COLLAB_CHECKPOINT_BACKUP_RECORD_KINDS = exports.COLLAB_CHECKPOINT_ARTIFACT_LIMITS = exports.encodeCollabTransferredMembershipRedemptionReceiptSigningInput = exports.encodeCollabTransferredMembershipClaimBatchDigestInput = exports.encodeCollabCloudToLanTargetCleanupProofSigningInput = exports.encodeCollabAuthorityRelinquishmentProofSigningInput = exports.decodeCollabTransferredMembershipRedemptionReceipt = exports.decodeCollabTransferredMembershipClaimCustodyReceipt = exports.decodeCollabTransferredMembershipClaimBatch = exports.decodeCollabTransferredMembershipClaim = exports.decodeCollabCloudToLanTargetCleanupProof = exports.decodeCollabAuthorityTransferStatus = exports.decodeCollabAuthorityTransferProposal = exports.decodeCollabAuthorityTransferOperationResponse = exports.decodeCollabAuthorityTransferOperationRequest = exports.decodeCollabAuthorityRelinquishmentProof = exports.COLLAB_LAN_TO_CLOUD_TRANSFER_PHASES = exports.COLLAB_CLOUD_TO_LAN_TRANSFER_PHASES = exports.COLLAB_AUTHORITY_TRANSFER_OPERATIONS = exports.COLLAB_AUTHORITY_TRANSFER_CANCELLATION_PHASES = exports.COLLAB_AUTHORITY_TRANSFER_CANCELLABLE_PHASES = exports.COLLAB_PROTOCOL_VERSION = exports.COLLAB_MEMBER_REF_PREFIX = exports.COLLAB_MAIN_REF = exports.COLLAB_LIMITS = void 0;
4
4
  exports.COLLAB_ERROR_CODES = exports.collabControlOperationCodec = exports.COLLAB_CONTROL_OPERATION_CODECS = exports.decodeCollabCloudProjectEventMessage = exports.COLLAB_CLOUD_EVENT_KINDS = exports.decodeCollabCloudProjectSnapshot = exports.COLLAB_CLOUD_PROJECT_SNAPSHOT_CODEC = exports.encodeDevelopmentBootstrapManifestCanonicalJson = exports.developmentBootstrapOperationCodec = exports.decodeDevelopmentBootstrapReport = exports.decodeDevelopmentBootstrapManifest = exports.DEVELOPMENT_BOOTSTRAP_OPERATION_CODECS = exports.DEVELOPMENT_BOOTSTRAP_OPERATIONS = exports.DEVELOPMENT_BOOTSTRAP_MANIFEST_SCHEMA_VERSION = exports.DEVELOPMENT_BOOTSTRAP_CANCELLATION_PHASES = exports.DEVELOPMENT_BOOTSTRAP_ATTEMPT_STATES = exports.DEVELOPMENT_BOOTSTRAP_ACTIVATION_PHASES = exports.matchCollabCloudRoute = exports.decodeCollabCloudSuccessEnvelope = exports.decodeCollabCloudProjectCheckpointExportStatus = exports.decodeCollabCloudErrorEnvelope = exports.decodeCollabCloudCapabilityDocument = exports.collabDevelopmentBootstrapRoute = exports.collabCloudSuccessEnvelope = exports.collabCloudProjectOperationRoute = exports.collabCloudProjectEventsRoute = exports.collabCloudProjectCheckpointExportRoute = exports.collabCloudProjectCheckpointExportArtifactRoute = exports.collabCloudGitRoute = exports.collabCloudErrorEnvelope = exports.collabCloudCapabilitiesRoute = exports.collabCloudCapabilitySupported = exports.collabCloudCapabilityDocument = exports.collabCloudAuthorityTransferArtifactRoute = exports.COLLAB_CLOUD_JSON_OPERATIONS = exports.COLLAB_CLOUD_CAPABILITY_DOCUMENT_SCHEMA_VERSION = exports.COLLAB_CLOUD_CAPABILITIES = exports.COLLAB_CLOUD_BINDING_VERSION = exports.COLLAB_CLOUD_BINDING_LIMITS = exports.decodeCollabProjectRetirementResult = exports.decodeCollabProjectRetirementRequest = exports.decodeCollabProjectRetirementOperationResponse = exports.decodeCollabProjectRetirementOperationRequest = exports.decodeCollabProjectRetirementAcknowledgement = exports.COLLAB_PROJECT_RETIREMENT_RESULT_KINDS = exports.COLLAB_PROJECT_RETIREMENT_OPERATIONS = exports.validateCollabProjectBackupCheckpointConsistency = exports.encodeCollabProjectBackupCheckpointManifestDigestInput = exports.encodeCollabProjectBackupCheckpointManifestCanonicalJson = exports.encodeCollabProjectBackupCheckpointCoordinationNdjson = void 0;
5
- exports.collabMemberRef = exports.isCollabProjectId = exports.isCollabOpaqueId = exports.isCollabMemberId = exports.isCollabGitOid = exports.scanCollabTicketReferences = exports.parseCollabTicketReferences = exports.decodeCollabProtocolEnvelope = exports.parseCollabMemberMentions = exports.sanitizeCollabDiagnosticContext = exports.collabErrorGroup = exports.CollabError = void 0;
5
+ exports.collabMemberRef = exports.isCollabProjectId = exports.isCollabOpaqueId = exports.isCollabMemberId = exports.isCollabGitOid = exports.decodeResolveTicketNumberResponse = exports.decodeResolveTicketNumber = exports.scanCollabTicketReferences = exports.parseCollabTicketReferences = exports.decodeCollabProtocolEnvelope = exports.parseCollabMemberMentions = exports.sanitizeCollabDiagnosticContext = exports.collabErrorGroup = exports.CollabError = void 0;
6
6
  var CollabConstants_1 = require("./CollabConstants");
7
7
  Object.defineProperty(exports, "COLLAB_LIMITS", { enumerable: true, get: function () { return CollabConstants_1.COLLAB_LIMITS; } });
8
8
  Object.defineProperty(exports, "COLLAB_MAIN_REF", { enumerable: true, get: function () { return CollabConstants_1.COLLAB_MAIN_REF; } });
@@ -125,6 +125,10 @@ Object.defineProperty(exports, "decodeCollabProtocolEnvelope", { enumerable: tru
125
125
  var CollabTicketReferenceParser_1 = require("./CollabTicketReferenceParser");
126
126
  Object.defineProperty(exports, "parseCollabTicketReferences", { enumerable: true, get: function () { return CollabTicketReferenceParser_1.parseCollabTicketReferences; } });
127
127
  Object.defineProperty(exports, "scanCollabTicketReferences", { enumerable: true, get: function () { return CollabTicketReferenceParser_1.scanCollabTicketReferences; } });
128
+ var CollabRequestTicketRequestCodecs_1 = require("./CollabRequestTicketRequestCodecs");
129
+ Object.defineProperty(exports, "decodeResolveTicketNumber", { enumerable: true, get: function () { return CollabRequestTicketRequestCodecs_1.decodeResolveTicketNumber; } });
130
+ var CollabRequestTicketResponseCodecs_1 = require("./CollabRequestTicketResponseCodecs");
131
+ Object.defineProperty(exports, "decodeResolveTicketNumberResponse", { enumerable: true, get: function () { return CollabRequestTicketResponseCodecs_1.decodeResolveTicketNumberResponse; } });
128
132
  var CollabValidation_1 = require("./CollabValidation");
129
133
  Object.defineProperty(exports, "isCollabGitOid", { enumerable: true, get: function () { return CollabValidation_1.isCollabGitOid; } });
130
134
  Object.defineProperty(exports, "isCollabMemberId", { enumerable: true, get: function () { return CollabValidation_1.isCollabMemberId; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudian-collab/protocol",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "Canonical Collab wire and Cloud binding contract for Claudian clients and servers.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -39,14 +39,13 @@
39
39
  "scripts": {
40
40
  "build": "npm run clean && tsc -p tsconfig.json && tsc -p tsconfig.esm.json && node scripts/finalize-esm.mjs",
41
41
  "check:compatibility": "node scripts/run-compatibility-check.mjs",
42
- "check:migration-provenance": "node scripts/check-migration-provenance.mjs --verify-standalone",
43
42
  "clean": "node scripts/clean.mjs",
44
43
  "lint": "eslint src tests scripts",
45
44
  "prepack": "npm run build",
46
45
  "release:candidate": "node scripts/release-candidate.mjs --output .context/release",
47
46
  "test": "jest && node --test tests/*.test.mjs",
48
47
  "typecheck": "tsc --noEmit -p tsconfig.jest.json",
49
- "verify": "npm run lint && npm run typecheck && npm run release:candidate && npm run test && npm run check:compatibility && npm run check:migration-provenance && npm run verify:pack",
48
+ "verify": "npm run lint && npm run typecheck && npm run release:candidate && npm run test && npm run check:compatibility && npm run verify:pack",
50
49
  "verify:pack": "node scripts/verify-pack.mjs"
51
50
  },
52
51
  "dependencies": {