@cleocode/core 2026.3.58 → 2026.3.60
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/dist/agents/agent-registry.d.ts +206 -0
- package/dist/agents/agent-registry.d.ts.map +1 -0
- package/dist/agents/agent-registry.js +288 -0
- package/dist/agents/agent-registry.js.map +1 -0
- package/dist/agents/agent-schema.js +5 -0
- package/dist/agents/agent-schema.js.map +1 -1
- package/dist/agents/execution-learning.js +474 -0
- package/dist/agents/execution-learning.js.map +1 -0
- package/dist/agents/health-monitor.d.ts +161 -0
- package/dist/agents/health-monitor.d.ts.map +1 -0
- package/dist/agents/health-monitor.js +217 -0
- package/dist/agents/health-monitor.js.map +1 -0
- package/dist/agents/index.d.ts +3 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +9 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/retry.d.ts +57 -4
- package/dist/agents/retry.d.ts.map +1 -1
- package/dist/agents/retry.js +57 -4
- package/dist/agents/retry.js.map +1 -1
- package/dist/backfill/index.d.ts +27 -0
- package/dist/backfill/index.d.ts.map +1 -1
- package/dist/backfill/index.js +229 -0
- package/dist/backfill/index.js.map +1 -0
- package/dist/bootstrap.d.ts +2 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +135 -28
- package/dist/bootstrap.js.map +1 -1
- package/dist/cleo.d.ts +40 -0
- package/dist/cleo.d.ts.map +1 -1
- package/dist/config.js +83 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1036 -536
- package/dist/index.js.map +4 -4
- package/dist/intelligence/adaptive-validation.js +497 -0
- package/dist/intelligence/adaptive-validation.js.map +1 -0
- package/dist/intelligence/impact.d.ts +34 -1
- package/dist/intelligence/impact.d.ts.map +1 -1
- package/dist/intelligence/impact.js +176 -0
- package/dist/intelligence/impact.js.map +1 -1
- package/dist/intelligence/index.d.ts +2 -2
- package/dist/intelligence/index.d.ts.map +1 -1
- package/dist/intelligence/index.js +6 -1
- package/dist/intelligence/index.js.map +1 -1
- package/dist/intelligence/types.d.ts +60 -0
- package/dist/intelligence/types.d.ts.map +1 -1
- package/dist/internal.d.ts +5 -4
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +11 -2
- package/dist/internal.js.map +1 -1
- package/dist/lib/index.d.ts +10 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +10 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/retry.d.ts +128 -0
- package/dist/lib/retry.d.ts.map +1 -0
- package/dist/lib/retry.js +152 -0
- package/dist/lib/retry.js.map +1 -0
- package/dist/nexus/sharing/index.d.ts +48 -2
- package/dist/nexus/sharing/index.d.ts.map +1 -1
- package/dist/nexus/sharing/index.js +110 -1
- package/dist/nexus/sharing/index.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +22 -2
- package/dist/scaffold.js.map +1 -1
- package/dist/sessions/session-enforcement.js +4 -0
- package/dist/sessions/session-enforcement.js.map +1 -1
- package/dist/stats/index.js +2 -0
- package/dist/stats/index.js.map +1 -1
- package/dist/stats/workflow-telemetry.d.ts +15 -0
- package/dist/stats/workflow-telemetry.d.ts.map +1 -1
- package/dist/stats/workflow-telemetry.js +400 -0
- package/dist/stats/workflow-telemetry.js.map +1 -0
- package/dist/store/brain-schema.js +4 -1
- package/dist/store/brain-schema.js.map +1 -1
- package/dist/store/converters.js +2 -0
- package/dist/store/converters.js.map +1 -1
- package/dist/store/cross-db-cleanup.d.ts +35 -0
- package/dist/store/cross-db-cleanup.d.ts.map +1 -1
- package/dist/store/cross-db-cleanup.js +169 -0
- package/dist/store/cross-db-cleanup.js.map +1 -0
- package/dist/store/db-helpers.js +2 -0
- package/dist/store/db-helpers.js.map +1 -1
- package/dist/store/migration-sqlite.js +5 -0
- package/dist/store/migration-sqlite.js.map +1 -1
- package/dist/store/sqlite-data-accessor.js +20 -28
- package/dist/store/sqlite-data-accessor.js.map +1 -1
- package/dist/store/sqlite.js +13 -2
- package/dist/store/sqlite.js.map +1 -1
- package/dist/store/task-store.js +4 -0
- package/dist/store/task-store.js.map +1 -1
- package/dist/store/tasks-schema.js +50 -20
- package/dist/store/tasks-schema.js.map +1 -1
- package/dist/tasks/add.js +87 -3
- package/dist/tasks/add.js.map +1 -1
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/complete.js +15 -4
- package/dist/tasks/complete.js.map +1 -1
- package/dist/tasks/enforcement.d.ts.map +1 -1
- package/dist/tasks/enforcement.js +8 -1
- package/dist/tasks/enforcement.js.map +1 -1
- package/dist/tasks/epic-enforcement.d.ts +61 -0
- package/dist/tasks/epic-enforcement.d.ts.map +1 -1
- package/dist/tasks/epic-enforcement.js +294 -0
- package/dist/tasks/epic-enforcement.js.map +1 -0
- package/dist/tasks/index.js +1 -1
- package/dist/tasks/index.js.map +1 -1
- package/dist/tasks/pipeline-stage.d.ts +70 -1
- package/dist/tasks/pipeline-stage.d.ts.map +1 -1
- package/dist/tasks/pipeline-stage.js +248 -0
- package/dist/tasks/pipeline-stage.js.map +1 -0
- package/dist/tasks/update.js +28 -0
- package/dist/tasks/update.js.map +1 -1
- package/package.json +5 -5
- package/schemas/config.schema.json +37 -1547
- package/src/__tests__/sharing.test.ts +24 -0
- package/src/agents/__tests__/agent-registry.test.ts +351 -0
- package/src/agents/__tests__/health-monitor.test.ts +332 -0
- package/src/agents/agent-registry.ts +394 -0
- package/src/agents/health-monitor.ts +279 -0
- package/src/agents/index.ts +24 -1
- package/src/agents/retry.ts +57 -4
- package/src/backfill/index.ts +27 -0
- package/src/bootstrap.ts +171 -30
- package/src/cleo.ts +103 -2
- package/src/config.ts +3 -3
- package/src/index.ts +1 -0
- package/src/intelligence/__tests__/impact.test.ts +165 -1
- package/src/intelligence/impact.ts +203 -0
- package/src/intelligence/index.ts +3 -0
- package/src/intelligence/types.ts +76 -0
- package/src/internal.ts +20 -0
- package/src/lib/__tests__/retry.test.ts +321 -0
- package/src/lib/index.ts +16 -0
- package/src/lib/retry.ts +224 -0
- package/src/nexus/sharing/index.ts +142 -2
- package/src/scaffold.ts +24 -2
- package/src/stats/workflow-telemetry.ts +15 -0
- package/src/store/__tests__/session-store.test.ts +43 -7
- package/src/store/__tests__/task-store.test.ts +1 -1
- package/src/store/__tests__/test-db-helper.ts +7 -3
- package/src/store/cross-db-cleanup.ts +35 -0
- package/src/tasks/__tests__/epic-enforcement.test.ts +9 -4
- package/src/tasks/__tests__/minimal-test.test.ts +2 -2
- package/src/tasks/__tests__/update.test.ts +25 -25
- package/src/tasks/complete.ts +11 -6
- package/src/tasks/enforcement.ts +6 -3
- package/src/tasks/epic-enforcement.ts +61 -0
- package/src/tasks/pipeline-stage.ts +70 -1
- package/templates/config.template.json +5 -116
- package/templates/global-config.template.json +2 -44
|
@@ -8,13 +8,19 @@
|
|
|
8
8
|
* - Blast radius calculation
|
|
9
9
|
* - Critical path detection
|
|
10
10
|
* - Edge cases (orphan tasks, circular refs, no deps)
|
|
11
|
+
* - predictImpact: free-text change description matching (T043)
|
|
11
12
|
*
|
|
12
13
|
* @module intelligence
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
import type { DataAccessor, Task } from '@cleocode/contracts';
|
|
16
17
|
import { describe, expect, it } from 'vitest';
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
analyzeChangeImpact,
|
|
20
|
+
analyzeTaskImpact,
|
|
21
|
+
calculateBlastRadius,
|
|
22
|
+
predictImpact,
|
|
23
|
+
} from '../impact.js';
|
|
18
24
|
|
|
19
25
|
// ============================================================================
|
|
20
26
|
// Test Helpers
|
|
@@ -451,3 +457,161 @@ describe('calculateBlastRadius', () => {
|
|
|
451
457
|
expect(result.transitiveCount).toBe(2);
|
|
452
458
|
});
|
|
453
459
|
});
|
|
460
|
+
|
|
461
|
+
// ============================================================================
|
|
462
|
+
// predictImpact (T043)
|
|
463
|
+
// ============================================================================
|
|
464
|
+
|
|
465
|
+
describe('predictImpact', () => {
|
|
466
|
+
it('returns empty report when no tasks match the change description', async () => {
|
|
467
|
+
const tasks = [
|
|
468
|
+
makeTask({ id: 'T001', title: 'Set up CI pipeline', description: 'Configure CI runner' }),
|
|
469
|
+
makeTask({ id: 'T002', title: 'Write unit tests', description: 'Add test coverage' }),
|
|
470
|
+
];
|
|
471
|
+
const acc = mockAccessor(tasks);
|
|
472
|
+
|
|
473
|
+
const result = await predictImpact('authentication login oauth', undefined, acc);
|
|
474
|
+
|
|
475
|
+
expect(result.change).toBe('authentication login oauth');
|
|
476
|
+
expect(result.matchedTasks).toHaveLength(0);
|
|
477
|
+
expect(result.affectedTasks).toHaveLength(0);
|
|
478
|
+
expect(result.totalAffected).toBe(0);
|
|
479
|
+
expect(result.summary).toContain('No tasks matched');
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('returns direct match when task title contains change keywords', async () => {
|
|
483
|
+
const tasks = [
|
|
484
|
+
makeTask({
|
|
485
|
+
id: 'T001',
|
|
486
|
+
title: 'Implement authentication module',
|
|
487
|
+
description: 'Add JWT auth',
|
|
488
|
+
}),
|
|
489
|
+
makeTask({ id: 'T002', title: 'Set up database schema', description: 'Create tables' }),
|
|
490
|
+
];
|
|
491
|
+
const acc = mockAccessor(tasks);
|
|
492
|
+
|
|
493
|
+
const result = await predictImpact('authentication module', undefined, acc);
|
|
494
|
+
|
|
495
|
+
expect(result.matchedTasks).toHaveLength(1);
|
|
496
|
+
expect(result.matchedTasks[0]?.id).toBe('T001');
|
|
497
|
+
expect(result.matchedTasks[0]?.exposure).toBe('direct');
|
|
498
|
+
expect(result.totalAffected).toBe(1);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('traces downstream dependents from matched seed tasks', async () => {
|
|
502
|
+
const tasks = [
|
|
503
|
+
makeTask({ id: 'T001', title: 'authentication service implementation', description: '' }),
|
|
504
|
+
makeTask({
|
|
505
|
+
id: 'T002',
|
|
506
|
+
title: 'User login form',
|
|
507
|
+
description: 'Depends on auth service',
|
|
508
|
+
depends: ['T001'],
|
|
509
|
+
}),
|
|
510
|
+
makeTask({
|
|
511
|
+
id: 'T003',
|
|
512
|
+
title: 'Dashboard access control',
|
|
513
|
+
description: 'Requires user login',
|
|
514
|
+
depends: ['T002'],
|
|
515
|
+
}),
|
|
516
|
+
makeTask({
|
|
517
|
+
id: 'T004',
|
|
518
|
+
title: 'Unrelated database migration',
|
|
519
|
+
description: 'Schema changes',
|
|
520
|
+
}),
|
|
521
|
+
];
|
|
522
|
+
const acc = mockAccessor(tasks);
|
|
523
|
+
|
|
524
|
+
const result = await predictImpact('authentication service', undefined, acc);
|
|
525
|
+
|
|
526
|
+
// T001 is the direct match (seed)
|
|
527
|
+
expect(result.matchedTasks.map((t) => t.id)).toContain('T001');
|
|
528
|
+
|
|
529
|
+
// T002 and T003 should be in affectedTasks as dependents/transitive
|
|
530
|
+
const ids = result.affectedTasks.map((t) => t.id);
|
|
531
|
+
expect(ids).toContain('T001');
|
|
532
|
+
expect(ids).toContain('T002');
|
|
533
|
+
expect(ids).toContain('T003');
|
|
534
|
+
// T004 is unrelated and should not appear
|
|
535
|
+
expect(ids).not.toContain('T004');
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('classifies exposure correctly: direct, dependent, transitive', async () => {
|
|
539
|
+
const tasks = [
|
|
540
|
+
makeTask({ id: 'T001', title: 'auth login service', description: '' }),
|
|
541
|
+
makeTask({ id: 'T002', title: 'Session management', description: '', depends: ['T001'] }),
|
|
542
|
+
makeTask({ id: 'T003', title: 'Profile page', description: '', depends: ['T002'] }),
|
|
543
|
+
];
|
|
544
|
+
const acc = mockAccessor(tasks);
|
|
545
|
+
|
|
546
|
+
const result = await predictImpact('auth login', undefined, acc);
|
|
547
|
+
|
|
548
|
+
const t1 = result.affectedTasks.find((t) => t.id === 'T001');
|
|
549
|
+
const t2 = result.affectedTasks.find((t) => t.id === 'T002');
|
|
550
|
+
const t3 = result.affectedTasks.find((t) => t.id === 'T003');
|
|
551
|
+
|
|
552
|
+
expect(t1?.exposure).toBe('direct');
|
|
553
|
+
expect(t2?.exposure).toBe('dependent');
|
|
554
|
+
expect(t3?.exposure).toBe('transitive');
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('sorts affectedTasks: direct first, then dependent, then transitive', async () => {
|
|
558
|
+
const tasks = [
|
|
559
|
+
makeTask({ id: 'T001', title: 'auth service core', description: '' }),
|
|
560
|
+
makeTask({ id: 'T002', title: 'Login controller', description: '', depends: ['T001'] }),
|
|
561
|
+
makeTask({ id: 'T003', title: 'Session store', description: '', depends: ['T002'] }),
|
|
562
|
+
];
|
|
563
|
+
const acc = mockAccessor(tasks);
|
|
564
|
+
|
|
565
|
+
const result = await predictImpact('auth service', undefined, acc);
|
|
566
|
+
|
|
567
|
+
const exposures = result.affectedTasks.map((t) => t.exposure);
|
|
568
|
+
// direct must come before dependent which must come before transitive
|
|
569
|
+
const directIdx = exposures.indexOf('direct');
|
|
570
|
+
const dependentIdx = exposures.indexOf('dependent');
|
|
571
|
+
const transitiveIdx = exposures.indexOf('transitive');
|
|
572
|
+
|
|
573
|
+
if (directIdx !== -1 && dependentIdx !== -1) {
|
|
574
|
+
expect(directIdx).toBeLessThan(dependentIdx);
|
|
575
|
+
}
|
|
576
|
+
if (dependentIdx !== -1 && transitiveIdx !== -1) {
|
|
577
|
+
expect(dependentIdx).toBeLessThan(transitiveIdx);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('respects matchLimit parameter', async () => {
|
|
582
|
+
const tasks = [
|
|
583
|
+
makeTask({ id: 'T001', title: 'auth module setup', description: '' }),
|
|
584
|
+
makeTask({ id: 'T002', title: 'auth token generation', description: '' }),
|
|
585
|
+
makeTask({ id: 'T003', title: 'auth session management', description: '' }),
|
|
586
|
+
];
|
|
587
|
+
const acc = mockAccessor(tasks);
|
|
588
|
+
|
|
589
|
+
// Limit to 1 seed
|
|
590
|
+
const result = await predictImpact('auth', undefined, acc, 1);
|
|
591
|
+
|
|
592
|
+
// Only 1 direct match seeded
|
|
593
|
+
expect(result.matchedTasks).toHaveLength(1);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('produces a meaningful summary string', async () => {
|
|
597
|
+
const tasks = [
|
|
598
|
+
makeTask({ id: 'T001', title: 'auth service', description: '' }),
|
|
599
|
+
makeTask({ id: 'T002', title: 'Login page', description: '', depends: ['T001'] }),
|
|
600
|
+
];
|
|
601
|
+
const acc = mockAccessor(tasks);
|
|
602
|
+
|
|
603
|
+
const result = await predictImpact('auth service', undefined, acc);
|
|
604
|
+
|
|
605
|
+
expect(result.summary).toContain('auth service');
|
|
606
|
+
expect(result.summary).toMatch(/\d+ task/);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('downstreamCount is 0 for leaf tasks with no dependents', async () => {
|
|
610
|
+
const tasks = [makeTask({ id: 'T001', title: 'auth service core', description: '' })];
|
|
611
|
+
const acc = mockAccessor(tasks);
|
|
612
|
+
|
|
613
|
+
const result = await predictImpact('auth service', undefined, acc);
|
|
614
|
+
|
|
615
|
+
expect(result.affectedTasks[0]?.downstreamCount).toBe(0);
|
|
616
|
+
});
|
|
617
|
+
});
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - Task impact assessment (direct + transitive dependents)
|
|
7
7
|
* - Change impact prediction (cancel, block, complete, reprioritize)
|
|
8
8
|
* - Blast radius calculation (scope quantification)
|
|
9
|
+
* - Free-text impact prediction (predictImpact) — T043
|
|
9
10
|
*
|
|
10
11
|
* @module intelligence
|
|
11
12
|
*/
|
|
@@ -22,6 +23,8 @@ import type {
|
|
|
22
23
|
ChangeImpact,
|
|
23
24
|
ChangeType,
|
|
24
25
|
ImpactAssessment,
|
|
26
|
+
ImpactedTask,
|
|
27
|
+
ImpactReport,
|
|
25
28
|
} from './types.js';
|
|
26
29
|
|
|
27
30
|
// ============================================================================
|
|
@@ -636,3 +639,203 @@ function generateRecommendation(
|
|
|
636
639
|
);
|
|
637
640
|
}
|
|
638
641
|
}
|
|
642
|
+
|
|
643
|
+
// ============================================================================
|
|
644
|
+
// Free-text Impact Prediction (T043)
|
|
645
|
+
// ============================================================================
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Score a task against a change description using simple keyword matching.
|
|
649
|
+
*
|
|
650
|
+
* Normalises both strings to lowercase and counts overlapping tokens (words).
|
|
651
|
+
* Returns a score in [0, 1] — 1 meaning every non-trivial token in the change
|
|
652
|
+
* description was found in the task text.
|
|
653
|
+
*/
|
|
654
|
+
function scoreTaskMatch(change: string, task: Task): number {
|
|
655
|
+
const STOP_WORDS = new Set([
|
|
656
|
+
'a',
|
|
657
|
+
'an',
|
|
658
|
+
'the',
|
|
659
|
+
'and',
|
|
660
|
+
'or',
|
|
661
|
+
'in',
|
|
662
|
+
'of',
|
|
663
|
+
'to',
|
|
664
|
+
'for',
|
|
665
|
+
'with',
|
|
666
|
+
'on',
|
|
667
|
+
'at',
|
|
668
|
+
'by',
|
|
669
|
+
'is',
|
|
670
|
+
'it',
|
|
671
|
+
'be',
|
|
672
|
+
'as',
|
|
673
|
+
'if',
|
|
674
|
+
'do',
|
|
675
|
+
'not',
|
|
676
|
+
]);
|
|
677
|
+
|
|
678
|
+
const tokenise = (text: string): string[] =>
|
|
679
|
+
text
|
|
680
|
+
.toLowerCase()
|
|
681
|
+
.split(/\W+/)
|
|
682
|
+
.filter((t) => t.length > 2 && !STOP_WORDS.has(t));
|
|
683
|
+
|
|
684
|
+
const changeTokens = new Set(tokenise(change));
|
|
685
|
+
if (changeTokens.size === 0) return 0;
|
|
686
|
+
|
|
687
|
+
const taskText = `${task.title ?? ''} ${task.description ?? ''}`;
|
|
688
|
+
const taskTokens = new Set(tokenise(taskText));
|
|
689
|
+
|
|
690
|
+
let matches = 0;
|
|
691
|
+
for (const token of changeTokens) {
|
|
692
|
+
if (taskTokens.has(token)) matches++;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return matches / changeTokens.size;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Predict the downstream impact of a free-text change description.
|
|
700
|
+
*
|
|
701
|
+
* Uses fuzzy keyword matching to identify candidate tasks that relate to
|
|
702
|
+
* the change, then walks the reverse dependency graph to enumerate all
|
|
703
|
+
* downstream tasks that may be affected.
|
|
704
|
+
*
|
|
705
|
+
* @remarks
|
|
706
|
+
* The matching is purely lexical (no embeddings). Tasks are ranked by
|
|
707
|
+
* how many tokens from the change description appear in their title and
|
|
708
|
+
* description. The top `matchLimit` (default: 5) matched tasks are used
|
|
709
|
+
* as seeds for downstream dependency tracing.
|
|
710
|
+
*
|
|
711
|
+
* @example
|
|
712
|
+
* ```ts
|
|
713
|
+
* import { predictImpact } from '@cleocode/core';
|
|
714
|
+
*
|
|
715
|
+
* const report = await predictImpact('Modify authentication flow', process.cwd());
|
|
716
|
+
* console.log(report.summary);
|
|
717
|
+
* // "3 tasks matched 'Modify authentication flow'; 7 downstream tasks affected."
|
|
718
|
+
* for (const task of report.affectedTasks) {
|
|
719
|
+
* console.log(`${task.id} (${task.exposure}): ${task.reason}`);
|
|
720
|
+
* }
|
|
721
|
+
* ```
|
|
722
|
+
*
|
|
723
|
+
* @param change - Free-text description of the proposed change (e.g. "Modify X")
|
|
724
|
+
* @param cwd - Working directory used to locate the tasks database
|
|
725
|
+
* @param accessor - Optional pre-created DataAccessor (useful in tests)
|
|
726
|
+
* @param matchLimit - Maximum number of seed tasks to match (default: 5)
|
|
727
|
+
* @returns Full impact prediction report
|
|
728
|
+
*/
|
|
729
|
+
export async function predictImpact(
|
|
730
|
+
change: string,
|
|
731
|
+
cwd?: string,
|
|
732
|
+
accessor?: DataAccessor,
|
|
733
|
+
matchLimit = 5,
|
|
734
|
+
): Promise<ImpactReport> {
|
|
735
|
+
const acc = accessor ?? (await getAccessor(cwd));
|
|
736
|
+
const tasks = await loadAllTasks(acc);
|
|
737
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
738
|
+
const dependentsMap = buildDependentsMap(tasks);
|
|
739
|
+
|
|
740
|
+
// --- Step 1: Score every task against the change description ---
|
|
741
|
+
const scored = tasks
|
|
742
|
+
.map((t) => ({ task: t, score: scoreTaskMatch(change, t) }))
|
|
743
|
+
.filter(({ score }) => score > 0)
|
|
744
|
+
.sort((a, b) => b.score - a.score);
|
|
745
|
+
|
|
746
|
+
const seeds = scored.slice(0, matchLimit).map(({ task }) => task);
|
|
747
|
+
|
|
748
|
+
if (seeds.length === 0) {
|
|
749
|
+
return {
|
|
750
|
+
change,
|
|
751
|
+
matchedTasks: [],
|
|
752
|
+
affectedTasks: [],
|
|
753
|
+
totalAffected: 0,
|
|
754
|
+
summary: `No tasks matched the change description "${change}".`,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// --- Step 2: Collect all affected task IDs via reverse dependency graph ---
|
|
759
|
+
const directMatchIds = new Set(seeds.map((t) => t.id));
|
|
760
|
+
|
|
761
|
+
// Map: taskId -> exposure level
|
|
762
|
+
const exposureMap = new Map<string, ImpactedTask['exposure']>();
|
|
763
|
+
for (const id of directMatchIds) {
|
|
764
|
+
exposureMap.set(id, 'direct');
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// BFS over dependents for each seed
|
|
768
|
+
for (const seed of seeds) {
|
|
769
|
+
const transitive = collectTransitiveDependents(seed.id, dependentsMap);
|
|
770
|
+
for (const depId of transitive) {
|
|
771
|
+
if (!exposureMap.has(depId)) {
|
|
772
|
+
// Determine whether this is a direct dependent of the seed or further out
|
|
773
|
+
const isDirectDependent = (dependentsMap.get(seed.id) ?? new Set()).has(depId);
|
|
774
|
+
exposureMap.set(depId, isDirectDependent ? 'dependent' : 'transitive');
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// --- Step 3: Build ImpactedTask list ---
|
|
780
|
+
const EXPOSURE_ORDER: Record<ImpactedTask['exposure'], number> = {
|
|
781
|
+
direct: 0,
|
|
782
|
+
dependent: 1,
|
|
783
|
+
transitive: 2,
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
const affectedTasks: ImpactedTask[] = [];
|
|
787
|
+
|
|
788
|
+
for (const [id, exposure] of exposureMap) {
|
|
789
|
+
const task = taskMap.get(id);
|
|
790
|
+
if (!task) continue;
|
|
791
|
+
|
|
792
|
+
const downstreamTransitive = collectTransitiveDependents(id, dependentsMap);
|
|
793
|
+
const downstreamCount = downstreamTransitive.size;
|
|
794
|
+
|
|
795
|
+
let reason: string;
|
|
796
|
+
if (exposure === 'direct') {
|
|
797
|
+
reason = `Task title/description matched "${change}".`;
|
|
798
|
+
} else if (exposure === 'dependent') {
|
|
799
|
+
const seedNames = seeds
|
|
800
|
+
.filter((s) => (dependentsMap.get(s.id) ?? new Set()).has(id))
|
|
801
|
+
.map((s) => s.id)
|
|
802
|
+
.join(', ');
|
|
803
|
+
reason = `Directly depends on matched task(s): ${seedNames}.`;
|
|
804
|
+
} else {
|
|
805
|
+
reason = 'Downstream of a matched task via transitive dependency chain.';
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
affectedTasks.push({
|
|
809
|
+
id,
|
|
810
|
+
title: task.title,
|
|
811
|
+
status: task.status,
|
|
812
|
+
priority: task.priority,
|
|
813
|
+
exposure,
|
|
814
|
+
downstreamCount,
|
|
815
|
+
reason,
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Sort: exposure order first, then descending downstream count
|
|
820
|
+
affectedTasks.sort((a, b) => {
|
|
821
|
+
const expDiff = EXPOSURE_ORDER[a.exposure] - EXPOSURE_ORDER[b.exposure];
|
|
822
|
+
if (expDiff !== 0) return expDiff;
|
|
823
|
+
return b.downstreamCount - a.downstreamCount;
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
const matchedTasks = affectedTasks.filter((t) => t.exposure === 'direct');
|
|
827
|
+
const totalAffected = affectedTasks.length;
|
|
828
|
+
|
|
829
|
+
const summary =
|
|
830
|
+
matchedTasks.length === 0
|
|
831
|
+
? `No tasks matched "${change}".`
|
|
832
|
+
: `${matchedTasks.length} task(s) matched "${change}"; ${totalAffected} total task(s) affected (including downstream).`;
|
|
833
|
+
|
|
834
|
+
return {
|
|
835
|
+
change,
|
|
836
|
+
matchedTasks,
|
|
837
|
+
affectedTasks,
|
|
838
|
+
totalAffected,
|
|
839
|
+
summary,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
@@ -30,6 +30,7 @@ export {
|
|
|
30
30
|
analyzeChangeImpact,
|
|
31
31
|
analyzeTaskImpact,
|
|
32
32
|
calculateBlastRadius,
|
|
33
|
+
predictImpact,
|
|
33
34
|
} from './impact.js';
|
|
34
35
|
// Patterns
|
|
35
36
|
export {
|
|
@@ -53,6 +54,8 @@ export type {
|
|
|
53
54
|
ChangeType,
|
|
54
55
|
DetectedPattern,
|
|
55
56
|
ImpactAssessment,
|
|
57
|
+
ImpactedTask,
|
|
58
|
+
ImpactReport,
|
|
56
59
|
LearningContext,
|
|
57
60
|
PatternExtractionOptions,
|
|
58
61
|
PatternMatch,
|
|
@@ -245,6 +245,82 @@ export interface AffectedTask {
|
|
|
245
245
|
reason: string;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
// ============================================================================
|
|
249
|
+
// Impact Prediction (free-text change description)
|
|
250
|
+
// ============================================================================
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* A single task predicted to be affected by a free-text change description.
|
|
254
|
+
*
|
|
255
|
+
* Produced by {@link predictImpact} after matching candidate tasks against
|
|
256
|
+
* the change description and running downstream dependency analysis.
|
|
257
|
+
*/
|
|
258
|
+
export interface ImpactedTask {
|
|
259
|
+
/** Task ID. */
|
|
260
|
+
id: string;
|
|
261
|
+
|
|
262
|
+
/** Task title. */
|
|
263
|
+
title: string;
|
|
264
|
+
|
|
265
|
+
/** Current task status. */
|
|
266
|
+
status: string;
|
|
267
|
+
|
|
268
|
+
/** Current task priority. */
|
|
269
|
+
priority: string;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Severity estimate for this task's exposure to the change.
|
|
273
|
+
*
|
|
274
|
+
* - `direct` — task title/description matched the change description
|
|
275
|
+
* - `dependent` — task depends on a matched task
|
|
276
|
+
* - `transitive` — downstream of a dependent via the dependency graph
|
|
277
|
+
*/
|
|
278
|
+
exposure: 'direct' | 'dependent' | 'transitive';
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Number of downstream tasks that depend on this task.
|
|
282
|
+
* Higher values indicate higher cascading risk.
|
|
283
|
+
*/
|
|
284
|
+
downstreamCount: number;
|
|
285
|
+
|
|
286
|
+
/** Why this task is predicted to be affected. */
|
|
287
|
+
reason: string;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Full impact prediction report for a free-text change description.
|
|
292
|
+
*
|
|
293
|
+
* Returned by {@link predictImpact}. Combines fuzzy task search with
|
|
294
|
+
* reverse dependency analysis to enumerate which tasks are at risk.
|
|
295
|
+
*/
|
|
296
|
+
export interface ImpactReport {
|
|
297
|
+
/** The original free-text change description. */
|
|
298
|
+
change: string;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Tasks directly matched by the change description (fuzzy search).
|
|
302
|
+
* These are the "seed" tasks from which downstream impact is traced.
|
|
303
|
+
*/
|
|
304
|
+
matchedTasks: ImpactedTask[];
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* All tasks predicted to be affected, ordered by exposure severity
|
|
308
|
+
* (direct first, then dependents, then transitive) and then by
|
|
309
|
+
* descending downstream count.
|
|
310
|
+
*/
|
|
311
|
+
affectedTasks: ImpactedTask[];
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Total count of distinct affected tasks (including direct matches).
|
|
315
|
+
*/
|
|
316
|
+
totalAffected: number;
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Human-readable summary of predicted impact scope.
|
|
320
|
+
*/
|
|
321
|
+
summary: string;
|
|
322
|
+
}
|
|
323
|
+
|
|
248
324
|
// ============================================================================
|
|
249
325
|
// Blast Radius
|
|
250
326
|
// ============================================================================
|
package/src/internal.ts
CHANGED
|
@@ -66,6 +66,7 @@ export {
|
|
|
66
66
|
analyzeChangeImpact,
|
|
67
67
|
analyzeTaskImpact,
|
|
68
68
|
calculateBlastRadius,
|
|
69
|
+
predictImpact,
|
|
69
70
|
} from './intelligence/impact.js';
|
|
70
71
|
export {
|
|
71
72
|
extractPatternsFromHistory,
|
|
@@ -87,6 +88,8 @@ export type {
|
|
|
87
88
|
ChangeType,
|
|
88
89
|
DetectedPattern,
|
|
89
90
|
ImpactAssessment,
|
|
91
|
+
ImpactedTask,
|
|
92
|
+
ImpactReport,
|
|
90
93
|
LearningContext,
|
|
91
94
|
PatternExtractionOptions,
|
|
92
95
|
PatternMatch,
|
|
@@ -98,6 +101,14 @@ export type {
|
|
|
98
101
|
|
|
99
102
|
// Issue
|
|
100
103
|
export { collectDiagnostics } from './issue/diagnostics.js';
|
|
104
|
+
// Lib — shared primitives
|
|
105
|
+
export {
|
|
106
|
+
computeDelay,
|
|
107
|
+
type RetryablePredicate,
|
|
108
|
+
type RetryContext,
|
|
109
|
+
type RetryOptions,
|
|
110
|
+
withRetry as withRetryShared,
|
|
111
|
+
} from './lib/retry.js';
|
|
101
112
|
export {
|
|
102
113
|
addChain,
|
|
103
114
|
advanceInstance,
|
|
@@ -611,6 +622,7 @@ export type {
|
|
|
611
622
|
AgentExecutionEvent,
|
|
612
623
|
AgentExecutionOutcome,
|
|
613
624
|
AgentHealthReport,
|
|
625
|
+
AgentHealthStatus,
|
|
614
626
|
AgentPerformanceSummary,
|
|
615
627
|
AgentRecoveryResult,
|
|
616
628
|
CapacitySummary,
|
|
@@ -623,12 +635,17 @@ export type {
|
|
|
623
635
|
} from './agents/index.js';
|
|
624
636
|
// Agents — runtime registry, health, retry, capacity
|
|
625
637
|
export {
|
|
638
|
+
// health-monitor functions (T039)
|
|
626
639
|
checkAgentHealth,
|
|
640
|
+
// registry / capacity / retry
|
|
627
641
|
classifyError,
|
|
628
642
|
createRetryPolicy,
|
|
629
643
|
DEFAULT_RETRY_POLICY,
|
|
630
644
|
deregisterAgent,
|
|
645
|
+
detectCrashedAgents,
|
|
646
|
+
detectStaleAgents,
|
|
631
647
|
findLeastLoadedAgent,
|
|
648
|
+
findStaleAgentRows,
|
|
632
649
|
generateAgentId,
|
|
633
650
|
getAgentErrorHistory,
|
|
634
651
|
getAgentInstance,
|
|
@@ -637,6 +654,7 @@ export {
|
|
|
637
654
|
getCapacitySummary,
|
|
638
655
|
getHealthReport,
|
|
639
656
|
getSelfHealingSuggestions,
|
|
657
|
+
HEARTBEAT_INTERVAL_MS,
|
|
640
658
|
heartbeat,
|
|
641
659
|
incrementTasksCompleted,
|
|
642
660
|
isOverloaded,
|
|
@@ -645,8 +663,10 @@ export {
|
|
|
645
663
|
processAgentLifecycleEvent,
|
|
646
664
|
recordAgentExecution,
|
|
647
665
|
recordFailurePattern,
|
|
666
|
+
recordHeartbeat,
|
|
648
667
|
recoverCrashedAgents,
|
|
649
668
|
registerAgent as registerAgentInstance,
|
|
669
|
+
STALE_THRESHOLD_MS,
|
|
650
670
|
storeHealingStrategy,
|
|
651
671
|
updateAgentStatus,
|
|
652
672
|
updateCapacity,
|