@playcraft/cli 0.1.70 → 0.1.72

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 (43) hide show
  1. package/README.md +20 -1
  2. package/dist/cli-root-help.js +1 -0
  3. package/dist/commands/references.js +3 -0
  4. package/dist/commands/skills.js +68 -4
  5. package/dist/commands/tools-batch.js +2 -0
  6. package/dist/project-skills/discovery.js +248 -0
  7. package/dist/project-skills/messages.js +80 -0
  8. package/dist/project-skills/remote-activation.js +500 -0
  9. package/dist/project-skills/remote-client.js +117 -0
  10. package/dist/project-skills/remote-commands.js +87 -0
  11. package/dist/project-skills/remote-prepare.js +223 -0
  12. package/dist/project-skills/search-command.js +47 -0
  13. package/dist/project-skills/search.js +96 -0
  14. package/dist/recording/project-digest.js +1 -1
  15. package/dist/references/messages.js +2 -0
  16. package/dist/references/protocol.js +39 -12
  17. package/dist/workspace-runtime/batch/freeze.js +94 -24
  18. package/dist/workspace-runtime/batch/records.js +12 -6
  19. package/dist/workspace-runtime/batch/routes.js +31 -11
  20. package/dist/workspace-runtime/batch/source-candidate.js +29 -16
  21. package/dist/workspace-runtime/batch/source-capture.js +251 -20
  22. package/dist/workspace-runtime/codex/app-server.js +30 -1
  23. package/dist/workspace-runtime/codex/model-broker.js +93 -6
  24. package/dist/workspace-runtime/codex/remix-references-broker.js +208 -67
  25. package/dist/workspace-runtime/codex/skill-discovery-broker.js +174 -0
  26. package/dist/workspace-runtime/serve.js +5 -0
  27. package/dist/workspace-runtime/server/directory-access.js +10 -0
  28. package/dist/workspace-runtime/server/dispatch.js +121 -28
  29. package/dist/workspace-runtime/server/http.js +345 -117
  30. package/dist/workspace-runtime/server/operation-record.js +3 -1
  31. package/dist/workspace-runtime/workspaces/composer-context.js +92 -41
  32. package/dist/workspace-runtime/workspaces/files.js +59 -23
  33. package/dist/workspace-runtime/workspaces/remix-references.js +162 -53
  34. package/dist/workspace-runtime/workspaces/skill-activation-recovery.js +54 -0
  35. package/dist/workspace-runtime/workspaces/skill-activation-requests.js +512 -0
  36. package/dist/workspace-runtime/workspaces/skills.js +13 -176
  37. package/dist/workspace-runtime/workspaces/starter-template.js +74 -0
  38. package/package.json +4 -3
  39. package/project-template/.claude/skills/playcraft-skill-recommender/SKILL.md +28 -6
  40. package/project-template-v2/.claude/skills/playcraft-skill-recommender/SKILL.md +28 -6
  41. package/project-templates/empty/gitignore +1 -0
  42. package/project-templates/phaser/.claude/skills/playcraft-skill-recommender/SKILL.md +28 -6
  43. package/project-templates/threejs/.claude/skills/playcraft-skill-recommender/SKILL.md +28 -6
@@ -6,11 +6,11 @@ import { workspaceRoot } from '../workspaces/registry.js';
6
6
  import { gitRead } from './git.js';
7
7
  import { captureIndependentSource } from './source-capture.js';
8
8
  import { filesystemObjectStore, readSnapshot, serializeFreeze, sourceCacheDir, writeSnapshot, } from './records.js';
9
- import { assertSourceIdentity, readSourceCandidate, writeSourceCandidate } from './source-candidate.js';
9
+ import { assertSourceIdentity, readSourceCandidate, writeSourceCandidate, } from './source-candidate.js';
10
10
  import { BatchSourceFreezeError, sourceFreezeError } from './source-failure.js';
11
11
  import { publishSourceCandidate } from './source-publish.js';
12
12
  import { clearDirectoryFreeze, withDirectoryFreeze } from '../server/directory-access.js';
13
- import { adoptPublicationBinding, publicationBinding, publicationGitOptions } from './publication.js';
13
+ import { adoptPublicationBinding, publicationBinding, publicationGitOptions, } from './publication.js';
14
14
  async function usagePercent(path) {
15
15
  const disk = await statfs(path);
16
16
  if (disk.blocks <= 0)
@@ -23,7 +23,7 @@ async function stopManagedWriters(state, workspaceId, directoryName) {
23
23
  for (const write of state.journal.listManagedWrites()) {
24
24
  if (write.stoppedAt || write.workspaceId !== scope)
25
25
  continue;
26
- if (write.pid === undefined || !await stopBackgroundWriter(write.pid))
26
+ if (write.pid === undefined || !(await stopBackgroundWriter(write.pid)))
27
27
  continue;
28
28
  await state.journal.putManagedWrite({ ...write, stoppedAt: new Date().toISOString() });
29
29
  }
@@ -42,8 +42,11 @@ export async function freezeBatchSource(state, body, options) {
42
42
  await clearDirectoryFreeze(state, request.batchId);
43
43
  return batchSourceFreezeResultSchema.parse({ status: 'frozen', snapshot: frozen });
44
44
  }
45
- return withDirectoryFreeze(state, { workspaceId: request.workspaceId,
46
- directoryName: request.sourceDirectoryName, batchId: request.batchId }, () => serializeFreeze(async () => {
45
+ return withDirectoryFreeze(state, {
46
+ workspaceId: request.workspaceId,
47
+ directoryName: request.sourceDirectoryName,
48
+ batchId: request.batchId,
49
+ }, () => serializeFreeze(async () => {
47
50
  let candidate;
48
51
  let source;
49
52
  try {
@@ -56,7 +59,12 @@ export async function freezeBatchSource(state, body, options) {
56
59
  return batchSourceFreezeResultSchema.parse({ status: 'frozen', snapshot: existing });
57
60
  }
58
61
  if (!pending && 'resumeOnly' in request && request.resumeOnly === true) {
59
- throw await sourceFreezeError(state, request.batchId, { code: 'batch_source_candidate_lost', stage: 'capture', retryable: false, outcome: 'unknown' });
62
+ throw await sourceFreezeError(state, request.batchId, {
63
+ code: 'batch_source_candidate_lost',
64
+ stage: 'capture',
65
+ retryable: false,
66
+ outcome: 'unknown',
67
+ });
60
68
  }
61
69
  if (pending)
62
70
  assertSourceIdentity(state, request, pending.snapshot, pending);
@@ -70,26 +78,40 @@ export async function freezeBatchSource(state, body, options) {
70
78
  if (pending)
71
79
  candidate = pending;
72
80
  else {
73
- const percent = Math.min(100, Math.max(0, options?.usagePercent ?? await usagePercent(source)));
81
+ const percent = Math.min(100, Math.max(0, options?.usagePercent ?? (await usagePercent(source))));
74
82
  if (workspaceDiskAdmission(percent) === 'paused')
75
83
  throw new Error('disk_admission_paused');
76
84
  await stopManagedWriters(state, request.workspaceId, request.sourceDirectoryName);
77
85
  const cache = sourceCacheDir(state, request.batchId);
78
86
  await rm(cache, { recursive: true, force: true });
79
- const captured = await captureIndependentSource(source, cache);
87
+ const captured = await captureIndependentSource(source, cache, request.starterTemplate, {
88
+ batchId: request.batchId,
89
+ workspaceId: request.workspaceId,
90
+ directoryName: request.sourceDirectoryName,
91
+ });
80
92
  // private
81
93
  await writeFile(`${cache}.digest`, captured.digest, { mode: 0o600 });
82
94
  const snapshot = {
83
- batchId: request.batchId, commitHash: captured.commitHash, digest: captured.digest,
84
- sourceWorkspaceId: request.workspaceId, sourceDirectoryName: request.sourceDirectoryName,
95
+ batchId: request.batchId,
96
+ commitHash: captured.commitHash,
97
+ digest: captured.digest,
98
+ sourceWorkspaceId: request.workspaceId,
99
+ sourceDirectoryName: request.sourceDirectoryName,
85
100
  frozenAt: new Date((await gitRead(cache, ['show', '-s', '--format=%cI', 'HEAD'])).trim()).toISOString(),
86
- ...(request.persist === 'remote_ref' ? { remoteRef: batchSourceRemoteRef(request.batchId) }
87
- : { artifactKey: `workspaces/${request.workspaceId}/batches/${request.batchId}/source/${captured.digest}.json.gz` }),
101
+ ...(request.persist === 'remote_ref'
102
+ ? { remoteRef: batchSourceRemoteRef(request.batchId) }
103
+ : {
104
+ artifactKey: `workspaces/${request.workspaceId}/batches/${request.batchId}/source/${captured.digest}.json.gz`,
105
+ }),
88
106
  };
89
107
  candidate = {
90
- version: 1, persist: request.persist, repositoryUrl: binding?.repositoryUrl ?? undefined, projectId: binding?.projectId,
91
- branchName: request.sourceDirectoryName === 'basic' ? binding?.branchName ?? undefined
92
- : binding?.directories?.find(directory => directory.directoryName === request.sourceDirectoryName)?.branchName ?? undefined,
108
+ version: 1,
109
+ persist: request.persist,
110
+ repositoryUrl: binding?.repositoryUrl ?? undefined,
111
+ projectId: binding?.projectId,
112
+ branchName: request.sourceDirectoryName === 'basic'
113
+ ? (binding?.branchName ?? undefined)
114
+ : (binding?.directories?.find((directory) => directory.directoryName === request.sourceDirectoryName)?.branchName ?? undefined),
93
115
  snapshot,
94
116
  };
95
117
  await writeSourceCandidate(state, candidate);
@@ -98,12 +120,44 @@ export async function freezeBatchSource(state, body, options) {
98
120
  catch (error) {
99
121
  if (error instanceof BatchSourceFreezeError)
100
122
  throw error;
101
- const known = new Set(['disk_admission_paused', 'disk_usage_unavailable', 'batch_source_changed', 'batch_source_path_invalid',
102
- 'workspace_not_registered', 'batch_source_identity_mismatch', 'batch_source_snapshot_mismatch']);
103
- const code = error instanceof Error && known.has(error.message) ? error.message : 'batch_source_capture_failed';
123
+ const known = new Set([
124
+ 'disk_admission_paused',
125
+ 'disk_usage_unavailable',
126
+ 'batch_source_changed',
127
+ 'batch_source_path_invalid',
128
+ 'workspace_not_registered',
129
+ 'batch_source_identity_mismatch',
130
+ 'batch_source_snapshot_mismatch',
131
+ 'batch_source_candidate_lost',
132
+ 'starter_template_unavailable',
133
+ 'starter_template_invalid',
134
+ 'starter_template_path_conflict',
135
+ 'batch_source_ignored_files',
136
+ ]);
137
+ const code = error instanceof Error && known.has(error.message)
138
+ ? error.message
139
+ : 'batch_source_capture_failed';
104
140
  throw await sourceFreezeError(state, request.batchId, {
105
- code, stage: 'capture', retryable: !['batch_source_path_invalid', 'batch_source_identity_mismatch', 'batch_source_snapshot_mismatch'].includes(code),
106
- outcome: code === 'batch_source_identity_mismatch' || code === 'batch_source_snapshot_mismatch' ? 'unknown' : 'not_published',
141
+ code,
142
+ stage: 'capture',
143
+ retryable: !(code === 'batch_source_changed' &&
144
+ request.starterTemplate &&
145
+ 'snapshot' in request.starterTemplate) &&
146
+ ![
147
+ 'batch_source_path_invalid',
148
+ 'batch_source_identity_mismatch',
149
+ 'batch_source_snapshot_mismatch',
150
+ 'batch_source_candidate_lost',
151
+ 'starter_template_unavailable',
152
+ 'starter_template_invalid',
153
+ 'starter_template_path_conflict',
154
+ 'batch_source_ignored_files',
155
+ ].includes(code),
156
+ outcome: code === 'batch_source_identity_mismatch' ||
157
+ code === 'batch_source_snapshot_mismatch' ||
158
+ code === 'batch_source_candidate_lost'
159
+ ? 'unknown'
160
+ : 'not_published',
107
161
  }, error);
108
162
  }
109
163
  await publishSourceCandidate(state, candidate, source, options?.objectStore ?? filesystemObjectStore(state), request.publication);
@@ -113,14 +167,30 @@ export async function freezeBatchSource(state, body, options) {
113
167
  await writeSnapshot(state, candidate.snapshot);
114
168
  }
115
169
  catch (error) {
116
- throw await sourceFreezeError(state, request.batchId, { code: 'batch_source_verification_failed', stage: 'verify', retryable: true, outcome: 'unknown' }, error);
170
+ throw await sourceFreezeError(state, request.batchId, {
171
+ code: 'batch_source_verification_failed',
172
+ stage: 'verify',
173
+ retryable: true,
174
+ outcome: 'unknown',
175
+ }, error);
117
176
  }
118
- return batchSourceFreezeResultSchema.parse({ status: 'frozen', snapshot: candidate.snapshot });
177
+ return batchSourceFreezeResultSchema.parse({
178
+ status: 'frozen',
179
+ snapshot: candidate.snapshot,
180
+ });
119
181
  }), () => stopManagedWriters(state, request.workspaceId, request.sourceDirectoryName), {
120
- signal: options?.signal, deadline: 'deadline' in request && typeof request.deadline === 'string' ? request.deadline : undefined,
182
+ signal: options?.signal,
183
+ deadline: 'deadline' in request && typeof request.deadline === 'string'
184
+ ? request.deadline
185
+ : undefined,
121
186
  }).catch(async (error) => {
122
187
  if (error instanceof Error && error.message === 'directory_access_cancelled')
123
- throw await sourceFreezeError(state, request.batchId, { code: 'source_freeze_timeout', stage: 'capture', retryable: false, outcome: 'not_published' });
188
+ throw await sourceFreezeError(state, request.batchId, {
189
+ code: 'source_freeze_timeout',
190
+ stage: 'capture',
191
+ retryable: false,
192
+ outcome: 'not_published',
193
+ });
124
194
  throw error;
125
195
  });
126
196
  }
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { mkdir, readFile, readdir, rename, writeFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
- import { batchSourceSnapshotRefSchema, sandboxBatchMemberStatusSchema, } from '@playcraft/common/cloud-workspace/runtime';
4
+ import { batchSourceSnapshotRefSchema, normalizeLegacyBatchSourceSnapshot, sandboxBatchMemberStatusSchema, } from '@playcraft/common/cloud-workspace/runtime';
5
5
  import { releaseBatchDirectory } from '../server/directory-access.js';
6
6
  const runs = new Map();
7
7
  let freezeChain = Promise.resolve();
@@ -29,7 +29,7 @@ export function sourceCacheDir(state, batchId) {
29
29
  return join(state.config.stateDir, 'batch-source-cache', batchId);
30
30
  }
31
31
  export function objectFile(state, key) {
32
- if (key.split('/').some(part => !part || part === '.' || part === '..'))
32
+ if (key.split('/').some((part) => !part || part === '.' || part === '..'))
33
33
  throw new Error('batch_object_key_invalid');
34
34
  return join(state.config.stateDir, 'batch-objects', key);
35
35
  }
@@ -53,7 +53,9 @@ async function readJson(path) {
53
53
  }
54
54
  export async function readSnapshot(state, batchId) {
55
55
  const value = await readJson(snapshotFile(state, batchId));
56
- return value ? batchSourceSnapshotRefSchema.parse(value) : undefined;
56
+ return value
57
+ ? batchSourceSnapshotRefSchema.parse(normalizeLegacyBatchSourceSnapshot(value))
58
+ : undefined;
57
59
  }
58
60
  export async function writeSnapshot(state, snapshot) {
59
61
  const parsed = batchSourceSnapshotRefSchema.parse(snapshot);
@@ -70,7 +72,7 @@ export async function writeMember(state, record) {
70
72
  }
71
73
  export async function listMembers(state) {
72
74
  const directory = join(state.config.stateDir, 'batch-members');
73
- const names = await readdir(directory).catch(error => {
75
+ const names = await readdir(directory).catch((error) => {
74
76
  if (error.code === 'ENOENT')
75
77
  return [];
76
78
  throw error;
@@ -111,7 +113,11 @@ export function memberStatus(record) {
111
113
  }
112
114
  export function filesystemObjectStore(state) {
113
115
  return {
114
- async put(key, body) { await writeAtomic(objectFile(state, key), body); },
115
- async get(key) { return readFile(objectFile(state, key)); },
116
+ async put(key, body) {
117
+ await writeAtomic(objectFile(state, key), body);
118
+ },
119
+ async get(key) {
120
+ return readFile(objectFile(state, key));
121
+ },
116
122
  };
117
123
  }
@@ -1,4 +1,4 @@
1
- import { batchSourceFreezeResultSchema } from '@playcraft/common/cloud-workspace/runtime';
1
+ import { batchSourceFreezeResultSchema, WORKSPACE_REMIX_REFERENCE_BODY_MAX_BYTES, } from '@playcraft/common/cloud-workspace/runtime';
2
2
  import { stopBatchMember, readBatchMemberStatus } from './control.js';
3
3
  import { deliverBatchMember } from './deliver.js';
4
4
  import { freezeBatchSource } from './freeze.js';
@@ -6,17 +6,28 @@ import { generateBatchMember } from './generate.js';
6
6
  import { prepareBatchMember } from './prepare.js';
7
7
  import { BatchSourceFreezeError } from './source-failure.js';
8
8
  const CLIENT_ERRORS = new Set([
9
- 'occupancy_required', 'invalid_batch_request', 'batch_branch_invalid', 'stale_owner', 'disk_admission_paused',
10
- 'batch_source_push_failed', 'batch_source_upload_failed', 'batch_source_changed', 'batch_source_path_invalid',
11
- 'batch_command_conflict', 'batch_snapshot_mismatch', 'batch_commit_mismatch', 'workspace_not_registered',
12
- 'disk_usage_unavailable', 'batch_object_key_invalid',
9
+ 'occupancy_required',
10
+ 'invalid_batch_request',
11
+ 'batch_branch_invalid',
12
+ 'stale_owner',
13
+ 'disk_admission_paused',
14
+ 'batch_source_push_failed',
15
+ 'batch_source_upload_failed',
16
+ 'batch_source_changed',
17
+ 'batch_source_path_invalid',
18
+ 'batch_command_conflict',
19
+ 'batch_snapshot_mismatch',
20
+ 'batch_commit_mismatch',
21
+ 'workspace_not_registered',
22
+ 'disk_usage_unavailable',
23
+ 'batch_object_key_invalid',
13
24
  ]);
14
- async function readBody(request) {
25
+ async function readBody(request, maxBytes = 1024 * 1024) {
15
26
  const chunks = [];
16
27
  let size = 0;
17
28
  for await (const chunk of request) {
18
29
  size += chunk.length;
19
- if (size > 1024 * 1024)
30
+ if (size > maxBytes)
20
31
  throw new Error('request_body_too_large');
21
32
  chunks.push(Buffer.from(chunk));
22
33
  }
@@ -30,7 +41,9 @@ export async function dispatchBatchRoute(state, request, url) {
30
41
  const member = /^\/runtime\/batch-members\/([^/]+)(?:\/(stop))?$/.exec(path);
31
42
  if (method === 'GET' && member && !member[2]) {
32
43
  const status = await readBatchMemberStatus(state, decodeURIComponent(member[1]));
33
- return status ? { status: 200, body: status } : { status: 404, body: { error: 'batch_member_not_found' } };
44
+ return status
45
+ ? { status: 200, body: status }
46
+ : { status: 404, body: { error: 'batch_member_not_found' } };
34
47
  }
35
48
  if (method !== 'POST')
36
49
  return undefined;
@@ -40,7 +53,7 @@ export async function dispatchBatchRoute(state, request, url) {
40
53
  const disconnected = () => controller.abort();
41
54
  request.socket.once('close', disconnected);
42
55
  try {
43
- const result = batchSourceFreezeResultSchema.parse(await freezeBatchSource(state, await readBody(request), { signal: controller.signal }));
56
+ const result = batchSourceFreezeResultSchema.parse(await freezeBatchSource(state, await readBody(request, WORKSPACE_REMIX_REFERENCE_BODY_MAX_BYTES), { signal: controller.signal }));
44
57
  return { status: 200, body: result };
45
58
  }
46
59
  finally {
@@ -54,7 +67,10 @@ export async function dispatchBatchRoute(state, request, url) {
54
67
  if (path === '/runtime/batch-members/deliver')
55
68
  return { status: 200, body: await deliverBatchMember(state, await readBody(request)) };
56
69
  if (member?.[2] === 'stop')
57
- return { status: 200, body: await stopBatchMember(state, decodeURIComponent(member[1]), await readBody(request)) };
70
+ return {
71
+ status: 200,
72
+ body: await stopBatchMember(state, decodeURIComponent(member[1]), await readBody(request)),
73
+ };
58
74
  return undefined;
59
75
  }
60
76
  catch (error) {
@@ -63,7 +79,11 @@ export async function dispatchBatchRoute(state, request, url) {
63
79
  const code = error instanceof Error && CLIENT_ERRORS.has(error.message) ? error.message : undefined;
64
80
  if (!code)
65
81
  throw error;
66
- const status = code === 'occupancy_required' || code === 'invalid_batch_request' || code === 'batch_branch_invalid' ? 400 : 409;
82
+ const status = code === 'occupancy_required' ||
83
+ code === 'invalid_batch_request' ||
84
+ code === 'batch_branch_invalid'
85
+ ? 400
86
+ : 409;
67
87
  return { status, body: { error: code } };
68
88
  }
69
89
  }
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { link, mkdir, open, readFile, unlink } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
- import { batchSourceSnapshotRefSchema, } from '@playcraft/common/cloud-workspace/runtime';
4
+ import { batchSourceSnapshotRefSchema, normalizeLegacyBatchSourceSnapshot, } from '@playcraft/common/cloud-workspace/runtime';
5
5
  import { publicationBinding } from './publication.js';
6
6
  export function sourceCandidateFile(state, batchId) {
7
7
  return join(state.config.stateDir, 'batch-source-candidates', `${batchId}.json`);
@@ -23,12 +23,16 @@ export async function readSourceCandidate(state, batchId) {
23
23
  catch {
24
24
  throw new Error('batch_source_snapshot_mismatch');
25
25
  }
26
- if (!stored || stored.version !== 1 || !['remote_ref', 'artifact'].includes(stored.persist)
27
- || (stored.repositoryUrl !== undefined && typeof stored.repositoryUrl !== 'string')
28
- || (stored.projectId !== undefined && stored.projectId !== null && typeof stored.projectId !== 'string')
29
- || (stored.branchName !== undefined && typeof stored.branchName !== 'string'))
26
+ if (!stored ||
27
+ stored.version !== 1 ||
28
+ !['remote_ref', 'artifact'].includes(stored.persist) ||
29
+ (stored.repositoryUrl !== undefined && typeof stored.repositoryUrl !== 'string') ||
30
+ (stored.projectId !== undefined &&
31
+ stored.projectId !== null &&
32
+ typeof stored.projectId !== 'string') ||
33
+ (stored.branchName !== undefined && typeof stored.branchName !== 'string'))
30
34
  throw new Error('batch_source_snapshot_mismatch');
31
- const parsed = batchSourceSnapshotRefSchema.safeParse(stored.snapshot);
35
+ const parsed = batchSourceSnapshotRefSchema.safeParse(normalizeLegacyBatchSourceSnapshot(stored.snapshot));
32
36
  if (!parsed.success)
33
37
  throw new Error('batch_source_snapshot_mismatch');
34
38
  const snapshot = parsed.data;
@@ -74,16 +78,25 @@ export async function writeSourceCandidate(state, candidate) {
74
78
  }
75
79
  export function assertSourceIdentity(state, request, snapshot, candidate) {
76
80
  const binding = publicationBinding(state, request.workspaceId, request.sourceDirectoryName, request.publication);
77
- const branchName = request.sourceDirectoryName === 'basic' ? binding?.branchName
78
- : binding?.directories?.find(directory => directory.directoryName === request.sourceDirectoryName)?.branchName;
79
- if (snapshot.batchId !== request.batchId || snapshot.sourceWorkspaceId !== request.workspaceId
80
- || snapshot.sourceDirectoryName !== request.sourceDirectoryName
81
- || (request.persist === 'remote_ref' ? !snapshot.remoteRef || Boolean(snapshot.artifactKey) : !snapshot.artifactKey || Boolean(snapshot.remoteRef))
82
- || (candidate && (candidate.persist !== request.persist || candidate.repositoryUrl !== (binding?.repositoryUrl ?? undefined)
83
- || candidate.projectId !== binding?.projectId || candidate.branchName !== (branchName ?? undefined)
84
- || candidate.snapshot.commitHash !== snapshot.commitHash || candidate.snapshot.digest !== snapshot.digest
85
- || candidate.snapshot.frozenAt !== snapshot.frozenAt || candidate.snapshot.remoteRef !== snapshot.remoteRef
86
- || candidate.snapshot.artifactKey !== snapshot.artifactKey))) {
81
+ const branchName = request.sourceDirectoryName === 'basic'
82
+ ? binding?.branchName
83
+ : binding?.directories?.find((directory) => directory.directoryName === request.sourceDirectoryName)?.branchName;
84
+ if (snapshot.batchId !== request.batchId ||
85
+ snapshot.sourceWorkspaceId !== request.workspaceId ||
86
+ snapshot.sourceDirectoryName !== request.sourceDirectoryName ||
87
+ (request.persist === 'remote_ref'
88
+ ? !snapshot.remoteRef || Boolean(snapshot.artifactKey)
89
+ : !snapshot.artifactKey || Boolean(snapshot.remoteRef)) ||
90
+ (candidate &&
91
+ (candidate.persist !== request.persist ||
92
+ candidate.repositoryUrl !== (binding?.repositoryUrl ?? undefined) ||
93
+ candidate.projectId !== binding?.projectId ||
94
+ candidate.branchName !== (branchName ?? undefined) ||
95
+ candidate.snapshot.commitHash !== snapshot.commitHash ||
96
+ candidate.snapshot.digest !== snapshot.digest ||
97
+ candidate.snapshot.frozenAt !== snapshot.frozenAt ||
98
+ candidate.snapshot.remoteRef !== snapshot.remoteRef ||
99
+ candidate.snapshot.artifactKey !== snapshot.artifactKey))) {
87
100
  throw new Error('batch_source_identity_mismatch');
88
101
  }
89
102
  }