gemstack-ai 1.3.0 → 1.4.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.
@@ -0,0 +1,639 @@
1
+ /**
2
+ * Gemstack Swarm Coordination & Validation Engine (Upgrade E)
3
+ *
4
+ * Implements deterministic swarm planning, exclusive write partitions,
5
+ * separation of duties (AUTHOR != REVIEWER), context capsule projection,
6
+ * and Upgrade C safety gate interception.
7
+ *
8
+ * ZERO RUNTIME DEPENDENCIES - Node.js built-ins exclusively.
9
+ */
10
+
11
+ const crypto = require('node:crypto');
12
+ const fs = require('node:fs');
13
+ const path = require('node:path');
14
+ const { normalizePath } = require('./hasher');
15
+ const { createFinding } = require('./findings');
16
+
17
+ const SWARM_SCHEMA_VERSION = '1.0.0';
18
+ const CANONICAL_ROLES = ['implementer', 'reviewer', 'security-auditor', 'coordinator'];
19
+ const DEFAULT_MAX_WORKERS = 4;
20
+ const HARD_MAX_WORKERS = 8;
21
+
22
+ /**
23
+ * Parses and validates a swarm manifest against canonical schema v1.0.0.
24
+ *
25
+ * @param {string|object} input - Raw JSON string or parsed object
26
+ * @returns {object} Canonical parsed manifest
27
+ */
28
+ function parseSwarmManifest(input) {
29
+ let parsed;
30
+ if (typeof input === 'string') {
31
+ try {
32
+ parsed = JSON.parse(input);
33
+ } catch (err) {
34
+ const error = new Error(`Invalid JSON in swarm manifest: ${err.message}`);
35
+ error.code = 'SWARM_PARSE_ERROR';
36
+ throw error;
37
+ }
38
+ } else if (input && typeof input === 'object') {
39
+ parsed = input;
40
+ } else {
41
+ const error = new Error('Swarm manifest input must be a JSON string or object.');
42
+ error.code = 'SWARM_INVALID_INPUT';
43
+ throw error;
44
+ }
45
+
46
+ return validateSwarmSchema(parsed);
47
+ }
48
+
49
+ /**
50
+ * Validates the schema structure of a parsed swarm manifest.
51
+ *
52
+ * @param {object} manifest
53
+ * @returns {object} Validated manifest
54
+ */
55
+ function validateSwarmSchema(manifest) {
56
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
57
+ const err = new Error('Swarm manifest root must be a JSON object.');
58
+ err.code = 'SWARM_INVALID_SCHEMA';
59
+ throw err;
60
+ }
61
+
62
+ if (manifest.version && manifest.version !== SWARM_SCHEMA_VERSION) {
63
+ const err = new Error(`Unsupported swarm schema version: "${manifest.version}". Expected "${SWARM_SCHEMA_VERSION}".`);
64
+ err.code = 'SWARM_INVALID_SCHEMA';
65
+ throw err;
66
+ }
67
+
68
+ if (!manifest.feature_id || typeof manifest.feature_id !== 'string') {
69
+ const err = new Error('Swarm manifest must declare a valid "feature_id".');
70
+ err.code = 'SWARM_INVALID_SCHEMA';
71
+ throw err;
72
+ }
73
+
74
+ if (!Array.isArray(manifest.waves)) {
75
+ const err = new Error('Swarm manifest must declare a "waves" array.');
76
+ err.code = 'SWARM_INVALID_SCHEMA';
77
+ throw err;
78
+ }
79
+
80
+ return manifest;
81
+ }
82
+
83
+ /**
84
+ * Serializes an object deterministically with UTF-16 code-unit sorted keys.
85
+ *
86
+ * @param {*} value
87
+ * @returns {string} Deterministic JSON string
88
+ */
89
+ function canonicalSerialize(value) {
90
+ if (value === null || typeof value !== 'object') {
91
+ return JSON.stringify(value);
92
+ }
93
+
94
+ if (Array.isArray(value)) {
95
+ const serializedItems = value.map(canonicalSerialize);
96
+ return '[' + serializedItems.join(',') + ']';
97
+ }
98
+
99
+ const keys = Object.keys(value).sort((a, b) => {
100
+ if (a < b) return -1;
101
+ if (a > b) return 1;
102
+ return 0;
103
+ });
104
+
105
+ const parts = [];
106
+ for (const k of keys) {
107
+ parts.push(JSON.stringify(k) + ':' + canonicalSerialize(value[k]));
108
+ }
109
+ return '{' + parts.join(',') + '}';
110
+ }
111
+
112
+ /**
113
+ * Validates worker IDs, role assignments, and checks that manifest is subordinate to specifications.
114
+ *
115
+ * @param {object} manifest
116
+ * @param {object} [authoritativeSources={}]
117
+ * @returns {{ valid: boolean, findings: Array<object> }}
118
+ */
119
+ function validateSwarmAuthority(manifest, authoritativeSources = {}) {
120
+ const findings = [];
121
+
122
+ // Check if manifest attempts to modify or contradict specification content
123
+ if (manifest.overrides_spec === true || manifest.alter_contracts === true) {
124
+ findings.push(createFinding({
125
+ code: 'SWARM_AUTHORITY_CONFLICT',
126
+ contractId: 'swarm-authority-subordinate',
127
+ phase: 'swarm',
128
+ location: `specs/${manifest.feature_id}/swarm.json`,
129
+ details: 'Swarm manifest attempted to declare authority overrides over specifications or contracts.'
130
+ }));
131
+ }
132
+
133
+ return {
134
+ valid: findings.length === 0,
135
+ findings
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Validates worker identity format and mechanical role permissions.
141
+ *
142
+ * @param {object} task
143
+ * @returns {{ valid: boolean, findings: Array<object> }}
144
+ */
145
+ function validateWorkerIdentityAndRole(task) {
146
+ const findings = [];
147
+ const workerId = task.worker_id;
148
+ const role = task.assigned_role;
149
+
150
+ if (!workerId || typeof workerId !== 'string' || workerId.trim().length === 0) {
151
+ findings.push(createFinding({
152
+ code: 'SWARM_ASSIGNMENT_INVALID',
153
+ contractId: 'swarm-authority-subordinate',
154
+ phase: 'swarm',
155
+ location: task.task_id || 'unknown',
156
+ details: 'Worker ID must be a non-empty explicit string.'
157
+ }));
158
+ }
159
+
160
+ if (!role || !CANONICAL_ROLES.includes(role)) {
161
+ findings.push(createFinding({
162
+ code: 'SWARM_ASSIGNMENT_INVALID',
163
+ contractId: 'swarm-authority-subordinate',
164
+ phase: 'swarm',
165
+ location: task.task_id || 'unknown',
166
+ details: `Assigned role "${role}" is not in canonical roles: ${CANONICAL_ROLES.join(', ')}.`
167
+ }));
168
+ }
169
+
170
+ return {
171
+ valid: findings.length === 0,
172
+ findings
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Validates that all tasks in swarm manifest reference valid tasks in tasks.md.
178
+ *
179
+ * @param {string} tasksContent - Content of tasks.md
180
+ * @param {object} manifest - Swarm manifest
181
+ * @returns {{ valid: boolean, findings: Array<object> }}
182
+ */
183
+ function resolveTaskOwnership(tasksContent, manifest) {
184
+ const findings = [];
185
+ const declaredTaskIds = new Set();
186
+
187
+ // Parse task IDs from tasks.md (e.g. "Task UE-T001", "TASK-001", "### UE-T001", etc.)
188
+ const lines = (tasksContent || '').split('\n');
189
+ for (const line of lines) {
190
+ const m = line.match(/\b(TASK-[A-Za-z0-9_-]+|UE-T[0-9]{3}|T[0-9]{3})\b/i);
191
+ if (m) {
192
+ declaredTaskIds.add(m[1].toUpperCase());
193
+ }
194
+ }
195
+
196
+ const waves = manifest.waves || [];
197
+ for (const wave of waves) {
198
+ const tasks = wave.tasks || [];
199
+ for (const t of tasks) {
200
+ const tid = (t.task_id || '').toUpperCase();
201
+ if (!declaredTaskIds.has(tid) && declaredTaskIds.size > 0) {
202
+ findings.push(createFinding({
203
+ code: 'SWARM_TASK_ORPHANED',
204
+ contractId: 'swarm-authority-subordinate',
205
+ phase: 'swarm',
206
+ location: t.task_id || 'unknown',
207
+ details: `Task "${t.task_id}" in swarm manifest is not declared in authoritative tasks.md.`
208
+ }));
209
+ }
210
+ }
211
+ }
212
+
213
+ return {
214
+ valid: findings.length === 0,
215
+ findings
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Plans and groups parallel tasks into concurrent waves ensuring disjoint write sets.
221
+ *
222
+ * @param {Array<object>} tasks - Array of task objects with task_id and write_set
223
+ * @returns {Array<object>} Scheduled waves
224
+ */
225
+ function planSwarmWaves(tasks) {
226
+ if (!Array.isArray(tasks)) return [];
227
+
228
+ const waves = [];
229
+ let remaining = [...tasks];
230
+
231
+ while (remaining.length > 0) {
232
+ const currentWaveTasks = [];
233
+ const currentWaveWriteSet = new Set();
234
+ const nextRemaining = [];
235
+
236
+ for (const task of remaining) {
237
+ const taskWrites = (task.write_set || []).map(normalizePath);
238
+ let collides = false;
239
+
240
+ for (const p of taskWrites) {
241
+ if (currentWaveWriteSet.has(p)) {
242
+ collides = true;
243
+ break;
244
+ }
245
+ }
246
+
247
+ if (!collides && currentWaveTasks.length < DEFAULT_MAX_WORKERS) {
248
+ currentWaveTasks.push(task);
249
+ for (const p of taskWrites) {
250
+ currentWaveWriteSet.add(p);
251
+ }
252
+ } else {
253
+ nextRemaining.push(task);
254
+ }
255
+ }
256
+
257
+ if (currentWaveTasks.length === 0 && nextRemaining.length > 0) {
258
+ // Force single task progress if deadlock occurs
259
+ currentWaveTasks.push(nextRemaining.shift());
260
+ }
261
+
262
+ waves.push({
263
+ wave_index: waves.length + 1,
264
+ status: 'PLANNED',
265
+ tasks: currentWaveTasks
266
+ });
267
+
268
+ remaining = nextRemaining;
269
+ }
270
+
271
+ return waves;
272
+ }
273
+
274
+ /**
275
+ * Validates that concurrent tasks in a wave declare mutually disjoint write sets.
276
+ *
277
+ * @param {object} wave - Swarm wave object
278
+ * @returns {{ valid: boolean, findings: Array<object> }}
279
+ */
280
+ function validateWritePartitions(wave) {
281
+ const findings = [];
282
+ const tasks = (wave && wave.tasks) || [];
283
+ const claimedPaths = new Map(); // path -> task_id
284
+
285
+ for (const t of tasks) {
286
+ const writes = (t.write_set || []).map(normalizePath);
287
+ for (const p of writes) {
288
+ if (claimedPaths.has(p)) {
289
+ const otherTaskId = claimedPaths.get(p);
290
+ findings.push(createFinding({
291
+ code: 'SWARM_WRITE_COLLISION',
292
+ contractId: 'exclusive-task-write-ownership',
293
+ phase: 'swarm',
294
+ location: p,
295
+ details: `Write collision on "${p}": concurrently claimed by "${otherTaskId}" and "${t.task_id}".`
296
+ }));
297
+ } else {
298
+ claimedPaths.set(p, t.task_id);
299
+ }
300
+ }
301
+ }
302
+
303
+ return {
304
+ valid: findings.length === 0,
305
+ findings
306
+ };
307
+ }
308
+
309
+ /**
310
+ * Enforces non-waivable separation of duties: AUTHOR != REVIEWER.
311
+ *
312
+ * @param {object} task - Task assignment with implementer and review block
313
+ * @returns {{ valid: boolean, findings: Array<object> }}
314
+ */
315
+ function validateReviewSeparation(task) {
316
+ const findings = [];
317
+ if (!task || !task.review) {
318
+ return { valid: true, findings: [] };
319
+ }
320
+
321
+ const authorId = (task.worker_id || '').trim();
322
+ const reviewerId = (task.review.reviewer_id || '').trim();
323
+
324
+ if (authorId && reviewerId && authorId.toLowerCase() === reviewerId.toLowerCase()) {
325
+ findings.push(createFinding({
326
+ code: 'SWARM_SELF_REVIEW_DETECTED',
327
+ contractId: 'author-not-reviewer',
328
+ phase: 'swarm',
329
+ location: task.task_id || 'unknown',
330
+ details: `Self-review detected on task "${task.task_id}": Author "${authorId}" is identical to Reviewer "${reviewerId}".`
331
+ }));
332
+ }
333
+
334
+ return {
335
+ valid: findings.length === 0,
336
+ findings
337
+ };
338
+ }
339
+
340
+ /**
341
+ * Enforces role-specific write-set boundaries (e.g. reviewer is read-only).
342
+ *
343
+ * @param {string} role - Worker role
344
+ * @param {Array<string>} modifiedFiles - List of files modified by worker
345
+ * @param {string} [taskId='unknown']
346
+ * @returns {{ valid: boolean, findings: Array<object> }}
347
+ */
348
+ function validateRoleWriteScope(role, modifiedFiles = [], taskId = 'unknown') {
349
+ const findings = [];
350
+ const normModified = (modifiedFiles || []).map(normalizePath);
351
+
352
+ if ((role === 'reviewer' || role === 'security-auditor') && normModified.length > 0) {
353
+ findings.push(createFinding({
354
+ code: 'SWARM_WRITE_SET_VIOLATION',
355
+ contractId: 'author-not-reviewer',
356
+ phase: 'swarm',
357
+ location: normModified[0],
358
+ details: `Role "${role}" on task "${taskId}" is strictly read-only but modified ${normModified.length} file(s).`
359
+ }));
360
+ }
361
+
362
+ if (role === 'visual-qa') {
363
+ for (const f of normModified) {
364
+ if (!f.includes('evidence/') && !f.endsWith('.png') && !f.endsWith('.json')) {
365
+ findings.push(createFinding({
366
+ code: 'SWARM_WRITE_SET_VIOLATION',
367
+ contractId: 'exclusive-task-write-ownership',
368
+ phase: 'swarm',
369
+ location: f,
370
+ details: `Role "visual-qa" is restricted to evidence directories but modified "${f}".`
371
+ }));
372
+ }
373
+ }
374
+ }
375
+
376
+ return {
377
+ valid: findings.length === 0,
378
+ findings
379
+ };
380
+ }
381
+
382
+ /**
383
+ * Projects a minimal, task-scoped context derived from context-capsule.json.
384
+ *
385
+ * @param {object} capsule - Loaded context-capsule.json object
386
+ * @param {string} taskId - Target task ID
387
+ * @param {string} [liveCapsuleHash=null] - Live SHA-256 hash of context-capsule.json
388
+ * @returns {object} Projected worker context
389
+ */
390
+ function projectWorkerContext(capsule, taskId, liveCapsuleHash = null) {
391
+ if (!capsule || typeof capsule !== 'object') {
392
+ const err = new Error('Capsule must be an object to project worker context.');
393
+ err.code = 'SWARM_CONTEXT_INVALID';
394
+ throw err;
395
+ }
396
+
397
+ const recordedHash = capsule.provenance ? capsule.provenance.source_set_hash : null;
398
+ const isStale = Boolean(liveCapsuleHash && recordedHash && liveCapsuleHash !== recordedHash);
399
+
400
+ // Extract only task-relevant items
401
+ const activeTasks = (capsule.active_feature && capsule.active_feature.active_tasks) || [];
402
+ const matchedTask = activeTasks.find(t => (t.id || '').toUpperCase() === (taskId || '').toUpperCase()) || null;
403
+
404
+ return {
405
+ task_id: taskId,
406
+ feature_id: capsule.active_feature ? capsule.active_feature.id : 'unknown',
407
+ source_capsule_hash: recordedHash,
408
+ freshness: isStale ? 'STALE' : 'FRESH',
409
+ canonical_invariants: capsule.canonical_invariants || [],
410
+ frozen_contracts: capsule.frozen_contracts || [],
411
+ task_metadata: matchedTask,
412
+ // Strictly exclude conversational chat history
413
+ chat_history: null,
414
+ developer_prompts: null
415
+ };
416
+ }
417
+
418
+ /**
419
+ * Validates worker count limits and rejects recursive spawning claims.
420
+ *
421
+ * @param {object} manifest - Swarm manifest
422
+ * @returns {{ valid: boolean, findings: Array<object> }}
423
+ */
424
+ function validateWorkerLimits(manifest) {
425
+ const findings = [];
426
+ const maxWorkers = manifest.max_workers || DEFAULT_MAX_WORKERS;
427
+
428
+ const waves = manifest.waves || [];
429
+ for (const wave of waves) {
430
+ const tasks = wave.tasks || [];
431
+ const workers = new Set(tasks.map(t => t.worker_id).filter(Boolean));
432
+
433
+ if (workers.size > maxWorkers || workers.size > HARD_MAX_WORKERS) {
434
+ findings.push(createFinding({
435
+ code: 'SWARM_WORKER_LIMIT_EXCEEDED',
436
+ contractId: 'swarm-authority-subordinate',
437
+ phase: 'swarm',
438
+ location: `Wave ${wave.wave_index}`,
439
+ details: `Wave ${wave.wave_index} declared ${workers.size} workers, exceeding maximum allowed (${maxWorkers}).`
440
+ }));
441
+ }
442
+
443
+ for (const t of tasks) {
444
+ if (t.spawned_children && t.spawned_children.length > 0) {
445
+ findings.push(createFinding({
446
+ code: 'SWARM_RECURSIVE_SPAWN_DENIED',
447
+ contractId: 'swarm-authority-subordinate',
448
+ phase: 'swarm',
449
+ location: t.task_id || 'unknown',
450
+ details: `Worker "${t.worker_id}" claimed spawned child workers. Recursive subagent spawning is prohibited.`
451
+ }));
452
+ }
453
+ }
454
+ }
455
+
456
+ return {
457
+ valid: findings.length === 0,
458
+ findings
459
+ };
460
+ }
461
+
462
+ /**
463
+ * Intercepts worker external tool actions through Upgrade C Provider Safety Gates.
464
+ *
465
+ * @param {object} workerAction - Action payload with provider_id and capability_id
466
+ * @param {object} [options={}] - Gate options
467
+ * @returns {object} Gate decision
468
+ */
469
+ function validateSwarmProviderSafety(workerAction, options = {}) {
470
+ const { evaluateProviderCapability } = require('./safety-gates');
471
+ const { createProviderRegistry } = require('./provider-registry');
472
+
473
+ // Build registry with mock provider for testing if none provided
474
+ const providers = options.providers || {
475
+ 'trusted-mock': {
476
+ type: 'MOCK',
477
+ mock_adapter: 'in-memory',
478
+ capabilities: {
479
+ 'mock_generation': { cost_state: 'FREE', estimated_unit_cost: 0.0 }
480
+ }
481
+ }
482
+ };
483
+ const registry = createProviderRegistry(providers);
484
+ const capResult = evaluateProviderCapability(workerAction, registry, options);
485
+
486
+ if (!capResult.authorized) {
487
+ return {
488
+ authorized: false,
489
+ code: 'SWARM_PROVIDER_UNAUTHORIZED',
490
+ message: capResult.message
491
+ };
492
+ }
493
+
494
+ if (workerAction.estimated_tokens && options.budget_limit) {
495
+ if (workerAction.estimated_tokens > options.budget_limit) {
496
+ return {
497
+ authorized: false,
498
+ code: 'SWARM_COST_LIMIT_EXCEEDED',
499
+ message: `Token usage ${workerAction.estimated_tokens} exceeds wave budget limit of ${options.budget_limit}.`
500
+ };
501
+ }
502
+ }
503
+
504
+ return {
505
+ authorized: true,
506
+ code: 'SWARM_PROVIDER_AUTHORIZED',
507
+ message: 'Worker provider action authorized.'
508
+ };
509
+ }
510
+
511
+ /**
512
+ * Validates a complete swarm.json file in read-only mode.
513
+ *
514
+ * @param {string} targetDir - Repository target directory
515
+ * @param {string} featureId - Active feature directory path
516
+ * @returns {{ valid: boolean, state: string, findings: Array<object> }}
517
+ */
518
+ function validateSwarmManifest(targetDir, featureId) {
519
+ const manifestPath = path.join(targetDir, featureId, 'swarm.json');
520
+ if (!fs.existsSync(manifestPath)) {
521
+ return {
522
+ valid: false,
523
+ state: 'MISSING',
524
+ findings: []
525
+ };
526
+ }
527
+
528
+ let raw;
529
+ try {
530
+ raw = fs.readFileSync(manifestPath, 'utf8');
531
+ } catch (err) {
532
+ return {
533
+ valid: false,
534
+ state: 'UNREADABLE',
535
+ findings: [createFinding({
536
+ code: 'SWARM_UNREADABLE',
537
+ contractId: 'swarm-authority-subordinate',
538
+ phase: 'swarm',
539
+ location: manifestPath,
540
+ details: err.message
541
+ })]
542
+ };
543
+ }
544
+
545
+ let manifest;
546
+ try {
547
+ manifest = parseSwarmManifest(raw);
548
+ } catch (err) {
549
+ return {
550
+ valid: false,
551
+ state: 'INVALID',
552
+ findings: [createFinding({
553
+ code: 'SWARM_INVALID_SCHEMA',
554
+ contractId: 'swarm-authority-subordinate',
555
+ phase: 'swarm',
556
+ location: manifestPath,
557
+ details: err.message
558
+ })]
559
+ };
560
+ }
561
+
562
+ const findings = [];
563
+
564
+ // 1. Authority validation
565
+ const authCheck = validateSwarmAuthority(manifest);
566
+ findings.push(...authCheck.findings);
567
+
568
+ // 2. Worker limits & recursive spawns
569
+ const limitsCheck = validateWorkerLimits(manifest);
570
+ findings.push(...limitsCheck.findings);
571
+
572
+ // 3. Per wave validations
573
+ const waves = manifest.waves || [];
574
+ for (const wave of waves) {
575
+ // Write collisions
576
+ const writeCheck = validateWritePartitions(wave);
577
+ findings.push(...writeCheck.findings);
578
+
579
+ // Tasks check
580
+ for (const t of wave.tasks || []) {
581
+ const idRoleCheck = validateWorkerIdentityAndRole(t);
582
+ findings.push(...idRoleCheck.findings);
583
+
584
+ const reviewCheck = validateReviewSeparation(t);
585
+ findings.push(...reviewCheck.findings);
586
+ }
587
+ }
588
+
589
+ // 4. Tasks.md ownership check if available
590
+ const tasksPath = path.join(targetDir, featureId, 'tasks.md');
591
+ if (fs.existsSync(tasksPath)) {
592
+ const tasksRaw = fs.readFileSync(tasksPath, 'utf8');
593
+ const ownerCheck = resolveTaskOwnership(tasksRaw, manifest);
594
+ findings.push(...ownerCheck.findings);
595
+ }
596
+
597
+ // 5. Context capsule freshness check if available
598
+ const capsulePath = path.join(targetDir, featureId, 'context-capsule.json');
599
+ if (fs.existsSync(capsulePath) && manifest.source_capsule_hash) {
600
+ const { hashFile } = require('./hasher');
601
+ const liveHash = hashFile(capsulePath);
602
+ if (liveHash !== manifest.source_capsule_hash) {
603
+ findings.push(createFinding({
604
+ code: 'SWARM_CONTEXT_STALE',
605
+ contractId: 'swarm-context-projected',
606
+ phase: 'swarm',
607
+ location: manifestPath,
608
+ details: `Swarm manifest source_capsule_hash (${manifest.source_capsule_hash.slice(0, 12)}...) is stale. Live: ${liveHash.slice(0, 12)}...`
609
+ }));
610
+ }
611
+ }
612
+
613
+ return {
614
+ valid: findings.length === 0,
615
+ state: findings.length === 0 ? 'VALID' : 'INVALID',
616
+ findings
617
+ };
618
+ }
619
+
620
+ module.exports = {
621
+ SWARM_SCHEMA_VERSION,
622
+ CANONICAL_ROLES,
623
+ DEFAULT_MAX_WORKERS,
624
+ HARD_MAX_WORKERS,
625
+ parseSwarmManifest,
626
+ validateSwarmSchema,
627
+ canonicalSerialize,
628
+ validateSwarmAuthority,
629
+ validateWorkerIdentityAndRole,
630
+ resolveTaskOwnership,
631
+ planSwarmWaves,
632
+ validateWritePartitions,
633
+ validateReviewSeparation,
634
+ validateRoleWriteScope,
635
+ projectWorkerContext,
636
+ validateWorkerLimits,
637
+ validateSwarmProviderSafety,
638
+ validateSwarmManifest
639
+ };