principles-disciple 1.66.0 → 1.67.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.
@@ -8,6 +8,8 @@ import { appendCandidateArtifactLineageRecord } from '../../src/core/nocturnal-a
8
8
  import { getImplementationAssetRoot } from '../../src/core/code-implementation-storage.js';
9
9
  import { EvolutionReducerImpl } from '../../src/core/evolution-reducer.js';
10
10
  import { saveLedger } from '../../src/core/principle-tree-ledger.js';
11
+ import { WorkflowFunnelLoader } from '../../src/core/workflow-funnel-loader.js';
12
+ import { RuntimeSummaryService } from '../../src/service/runtime-summary-service.js';
11
13
 
12
14
  const tempDirs: string[] = [];
13
15
 
@@ -338,4 +340,290 @@ describe('evolution commands', () => {
338
340
 
339
341
  expect(result.text).toContain('internalization routes: P-001:skill@');
340
342
  });
343
+
344
+ it('renders workflowFunnel blocks when YAML-driven funnels are present', () => {
345
+ const workspace = makeTempDir();
346
+ const stateDir = path.join(workspace, '.state');
347
+
348
+ // Write workflows.yaml with two funnels matching actual schema
349
+ const workflowsYaml = `
350
+ version: "1.0"
351
+ funnels:
352
+ - workflowId: nocturnal
353
+ stages:
354
+ - name: dreamer_completed
355
+ eventType: nocturnal_dreamer_completed
356
+ eventCategory: completed
357
+ statsField: evolution.nocturnalDreamerCompleted
358
+ - name: artifact_persisted
359
+ eventType: nocturnal_artifact_persisted
360
+ eventCategory: completed
361
+ statsField: evolution.nocturnalArtifactPersisted
362
+ - workflowId: rulehost
363
+ stages:
364
+ - name: evaluated
365
+ eventType: rulehost_evaluated
366
+ eventCategory: evaluated
367
+ statsField: evolution.rulehostEvaluated
368
+ `;
369
+ fs.writeFileSync(path.join(stateDir, 'workflows.yaml'), workflowsYaml, 'utf8');
370
+
371
+ // Write daily stats with matching event counts (must be in logs/ subdirectory)
372
+ const today = new Date().toISOString().slice(0, 10);
373
+ writeJson(path.join(stateDir, 'logs', 'daily-stats.json'), {
374
+ [today]: {
375
+ evolution: {
376
+ nocturnalDreamerCompleted: 3,
377
+ nocturnalArtifactPersisted: 2,
378
+ rulehostEvaluated: 15,
379
+ },
380
+ },
381
+ });
382
+
383
+ const result = handleEvolutionStatusCommand({
384
+ config: { workspaceDir: workspace, language: 'en' },
385
+ } as any);
386
+
387
+ expect(result.text).toContain('Workflow Funnel: nocturnal');
388
+ expect(result.text).toMatch(/dreamer_completed: 3/);
389
+ expect(result.text).toMatch(/artifact_persisted: 2/);
390
+ expect(result.text).toContain('Workflow Funnel: rulehost');
391
+ expect(result.text).toMatch(/evaluated: 15/);
392
+ });
393
+
394
+ it('skips funnel block when workflowFunnels is empty array', () => {
395
+ const workspace = makeTempDir();
396
+
397
+ // Write empty funnels array — valid YAML but no funnels defined
398
+ const workflowsYaml = `version: "1.0"\nfunnels: []`;
399
+ fs.writeFileSync(path.join(workspace, '.state', 'workflows.yaml'), workflowsYaml, 'utf8');
400
+
401
+ const result = handleEvolutionStatusCommand({
402
+ config: { workspaceDir: workspace, language: 'en' },
403
+ } as any);
404
+
405
+ expect(result.text).not.toContain('Workflow Funnel:');
406
+ });
407
+
408
+ it('shows degraded status and warning when YAML load has warnings', () => {
409
+ const workspace = makeTempDir();
410
+ const stateDir = path.join(workspace, '.state');
411
+
412
+ // Write malformed YAML — loader emits a parse warning
413
+ const badYaml = `
414
+ version: "1.0"
415
+ funnels:
416
+ - workflowId: nocturnal
417
+ stages:
418
+ - name: "unclosed string
419
+ `;
420
+ fs.writeFileSync(path.join(stateDir, 'workflows.yaml'), badYaml, 'utf8');
421
+
422
+ const result = handleEvolutionStatusCommand({
423
+ config: { workspaceDir: workspace, language: 'en' },
424
+ } as any);
425
+
426
+ // Should still render — degraded but not crashed
427
+ expect(result.text).toContain('Evolution Status');
428
+ // Loader warning should appear in output
429
+ expect(result.text).toMatch(/YAML load warning|YAML parse error|workflows\.yaml/);
430
+ });
431
+
432
+ it('renders Chinese workflow funnel labels from YAML', () => {
433
+ const workspace = makeTempDir();
434
+ const stateDir = path.join(workspace, '.state');
435
+
436
+ const workflowsYaml = `
437
+ version: "1.0"
438
+ funnels:
439
+ - workflowId: nocturnal
440
+ stages:
441
+ - name: 做梦完成
442
+ eventType: nocturnal_dreamer_completed
443
+ eventCategory: completed
444
+ statsField: evolution.nocturnalDreamerCompleted
445
+ `;
446
+ fs.writeFileSync(path.join(stateDir, 'workflows.yaml'), workflowsYaml, 'utf8');
447
+
448
+ const today = new Date().toISOString().slice(0, 10);
449
+ writeJson(path.join(stateDir, 'logs', 'daily-stats.json'), {
450
+ [today]: {
451
+ evolution: { nocturnalDreamerCompleted: 7 },
452
+ },
453
+ });
454
+
455
+ const result = handleEvolutionStatusCommand({
456
+ config: { workspaceDir: workspace, language: 'zh' },
457
+ } as any);
458
+
459
+ expect(result.text).toContain('Workflow 漏斗: nocturnal');
460
+ expect(result.text).toMatch(/做梦完成: 7/);
461
+ });
462
+ });
463
+
464
+ describe('YAML funnel E2E integration tests', () => {
465
+ // TEST-01: Full YAML-driven flow with real WorkflowFunnelLoader
466
+ it('e2e_test_full_yaml_driven_flow', () => {
467
+ const workspace = makeTempDir();
468
+ const stateDir = path.join(workspace, '.state');
469
+
470
+ // Create real WorkflowFunnelLoader (not mocked)
471
+ const loader = new WorkflowFunnelLoader(stateDir);
472
+ loader.watch();
473
+
474
+ // Write valid workflows.yaml
475
+ const workflowsYaml = `
476
+ version: "1.0"
477
+ funnels:
478
+ - workflowId: nocturnal
479
+ stages:
480
+ - name: dreamer_completed
481
+ eventType: nocturnal_dreamer_completed
482
+ eventCategory: completed
483
+ statsField: evolution.nocturnalDreamerCompleted
484
+ - name: artifact_persisted
485
+ eventType: nocturnal_artifact_persisted
486
+ eventCategory: completed
487
+ statsField: evolution.nocturnalArtifactPersisted
488
+ - workflowId: rulehost
489
+ stages:
490
+ - name: evaluated
491
+ eventType: rulehost_evaluated
492
+ eventCategory: evaluated
493
+ statsField: evolution.rulehostEvaluated
494
+ `;
495
+ fs.writeFileSync(path.join(stateDir, 'workflows.yaml'), workflowsYaml, 'utf8');
496
+ loader.load(); // reload after writing file
497
+
498
+ // Write daily-stats.json
499
+ const today = new Date().toISOString().slice(0, 10);
500
+ writeJson(path.join(stateDir, 'logs', 'daily-stats.json'), {
501
+ [today]: {
502
+ evolution: {
503
+ nocturnalDreamerCompleted: 3,
504
+ nocturnalArtifactPersisted: 2,
505
+ rulehostEvaluated: 15,
506
+ },
507
+ },
508
+ });
509
+
510
+ // Call getSummary directly with real loader data
511
+ const summary = RuntimeSummaryService.getSummary(workspace, {
512
+ funnels: loader.getAllFunnels(),
513
+ loaderWarnings: loader.getWarnings(),
514
+ });
515
+
516
+ // Assert workflowFunnels structure
517
+ expect(summary.workflowFunnels).toBeDefined();
518
+ expect(summary.workflowFunnels!.length).toBe(2);
519
+ expect(summary.workflowFunnels![0].funnelKey).toBe('nocturnal');
520
+ expect(summary.workflowFunnels![0].stages[0].label).toBe('dreamer_completed');
521
+ expect(summary.workflowFunnels![0].stages[0].count).toBe(3);
522
+ expect(summary.workflowFunnels![0].stages[1].label).toBe('artifact_persisted');
523
+ expect(summary.workflowFunnels![0].stages[1].count).toBe(2);
524
+ expect(summary.workflowFunnels![1].funnelKey).toBe('rulehost');
525
+ expect(summary.workflowFunnels![1].stages[0].label).toBe('evaluated');
526
+ expect(summary.workflowFunnels![1].stages[0].count).toBe(15);
527
+ expect(summary.workflowFunnels![0].funnelLabel).toBe('nocturnal');
528
+
529
+ // DEGRADED-01: valid YAML + valid stats → status ok, no funnel-related warnings
530
+ expect(summary.metadata.status).toBe('ok');
531
+ // Note: warnings array may contain non-funnel warnings (GFI/Daily stats defaults); funnel warnings are checked separately
532
+ const funnelWarnings = summary.metadata.warnings.filter(w => w.includes('statsField') || w.includes('YAML load'));
533
+ expect(funnelWarnings).toHaveLength(0);
534
+
535
+ loader.dispose();
536
+ });
537
+
538
+ // TEST-02: Degraded fallback when YAML is missing
539
+ it('e2e_test_degraded_fallback_on_missing_yaml', () => {
540
+ const workspace = makeTempDir();
541
+ const stateDir = path.join(workspace, '.state');
542
+
543
+ // Create real loader pointing at stateDir with NO workflows.yaml
544
+ const loader = new WorkflowFunnelLoader(stateDir);
545
+ loader.watch();
546
+
547
+ // Write daily-stats.json (so the file exists, but no YAML)
548
+ const today = new Date().toISOString().slice(0, 10);
549
+ writeJson(path.join(stateDir, 'logs', 'daily-stats.json'), {
550
+ [today]: { evolution: {} },
551
+ });
552
+
553
+ // Call getSummary — should not crash
554
+ const summary = RuntimeSummaryService.getSummary(workspace, {
555
+ funnels: loader.getAllFunnels(),
556
+ loaderWarnings: loader.getWarnings(),
557
+ });
558
+
559
+ // Assert degraded status
560
+ expect(summary.metadata.status).toBe('degraded');
561
+ // loaderWarnings are prefixed with "YAML load warning: " when propagated to metadata.warnings
562
+ expect(summary.metadata.warnings).toContain('YAML load warning: workflows.yaml file not found.');
563
+ // DEGRADED-02: funnels absent/empty when YAML missing — empty array is acceptable, just not rendered
564
+ expect(summary.workflowFunnels == null || summary.workflowFunnels.length === 0).toBe(true);
565
+
566
+ loader.dispose();
567
+ });
568
+
569
+ // TEST-03: Hot-reload — YAML changes reflected after loader.load()
570
+ it('e2e_test_hot_reload_reflects_yaml_changes', () => {
571
+ const workspace = makeTempDir();
572
+ const stateDir = path.join(workspace, '.state');
573
+
574
+ const loader = new WorkflowFunnelLoader(stateDir);
575
+ loader.watch();
576
+
577
+ // Write initial workflows.yaml with original_label
578
+ const workflowsYamlV1 = `
579
+ version: "1.0"
580
+ funnels:
581
+ - workflowId: nocturnal
582
+ stages:
583
+ - name: original_label
584
+ eventType: nocturnal_dreamer_completed
585
+ eventCategory: completed
586
+ statsField: evolution.nocturnalDreamerCompleted
587
+ `;
588
+ const yamlPath = path.join(stateDir, 'workflows.yaml');
589
+ fs.writeFileSync(yamlPath, workflowsYamlV1, 'utf8');
590
+ loader.load();
591
+
592
+ const today = new Date().toISOString().slice(0, 10);
593
+ writeJson(path.join(stateDir, 'logs', 'daily-stats.json'), {
594
+ [today]: { evolution: { nocturnalDreamerCompleted: 5 } },
595
+ });
596
+
597
+ // First call — should show original_label
598
+ const summary1 = RuntimeSummaryService.getSummary(workspace, {
599
+ funnels: loader.getAllFunnels(),
600
+ loaderWarnings: loader.getWarnings(),
601
+ });
602
+ expect(summary1.workflowFunnels![0].stages[0].label).toBe('original_label');
603
+ expect(summary1.workflowFunnels![0].stages[0].count).toBe(5);
604
+
605
+ // Modify workflows.yaml to use modified_label
606
+ const workflowsYamlV2 = `
607
+ version: "1.0"
608
+ funnels:
609
+ - workflowId: nocturnal
610
+ stages:
611
+ - name: modified_label
612
+ eventType: nocturnal_dreamer_completed
613
+ eventCategory: completed
614
+ statsField: evolution.nocturnalDreamerCompleted
615
+ `;
616
+ fs.writeFileSync(yamlPath, workflowsYamlV2, 'utf8');
617
+ loader.load(); // trigger hot-reload manually
618
+
619
+ // Second call — should show modified_label
620
+ const summary2 = RuntimeSummaryService.getSummary(workspace, {
621
+ funnels: loader.getAllFunnels(),
622
+ loaderWarnings: loader.getWarnings(),
623
+ });
624
+ expect(summary2.workflowFunnels![0].stages[0].label).toBe('modified_label');
625
+ expect(summary2.workflowFunnels![0].stages[0].count).toBe(5);
626
+
627
+ loader.dispose();
628
+ });
341
629
  });
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import { EventLogService, EventLog } from '../../src/core/event-log.js';
3
- import type { DailyStats, DeepReflectionEventData, DiagnosticianReportEventData } from '../../src/types/event-types.js';
3
+ import type { DailyStats, DiagnosticianReportEventData } from '../../src/types/event-types.js';
4
4
  import * as fs from 'fs';
5
5
  import * as path from 'path';
6
6
  import * as os from 'os';
@@ -19,32 +19,6 @@ describe('EventLog', () => {
19
19
  fs.rmSync(tempDir, { recursive: true, force: true });
20
20
  });
21
21
 
22
- describe('recordDeepReflection', () => {
23
- it('should record deep reflection event', () => {
24
- const data: DeepReflectionEventData = {
25
- modelId: 'claude-sonnet-4',
26
- modelSelectionMode: 'auto',
27
- confidenceScore: 0.85,
28
- insightsGenerated: 3,
29
- durationMs: 1500,
30
- passed: true
31
- };
32
-
33
- eventLog.recordDeepReflection('session-1', data);
34
- eventLog.flush();
35
-
36
- const today = new Date().toISOString().slice(0, 10);
37
- const eventsFile = path.join(tempDir, 'logs', `events_${today}.jsonl`);
38
- const content = fs.readFileSync(eventsFile, 'utf-8');
39
- const event = JSON.parse(content.trim());
40
-
41
- expect(event.type).toBe('deep_reflection');
42
- expect(event.category).toBe('passed');
43
- expect(event.data.modelId).toBe('claude-sonnet-4');
44
- expect(event.data.modelSelectionMode).toBe('auto');
45
- });
46
- });
47
-
48
22
  describe('DailyStats', () => {
49
23
  it('should aggregate tool call statistics correctly', () => {
50
24
  // Record multiple tool calls
@@ -97,8 +71,6 @@ describe('EventLog', () => {
97
71
  // Hooks field
98
72
  expect(stats.hooks).toBeDefined();
99
73
 
100
- // Deep Reflection field
101
- expect(stats.deepReflection).toBeDefined();
102
74
  });
103
75
 
104
76
  it('should increment tools.failure on error', () => {
@@ -678,7 +678,7 @@ describe('RuntimeSummaryService', () => {
678
678
  last_updated: '2026-03-20T10:00:00Z',
679
679
  });
680
680
  writeJson(path.join(workspace, '.state', 'logs', 'daily-stats.json'), {
681
- '2026-03-20': {
681
+ [new Date().toISOString().slice(0, 10)]: {
682
682
  toolCalls: 120,
683
683
  painSignals: 15,
684
684
  evolutionTasks: 5,