principles-disciple 1.40.0 → 1.42.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.
Files changed (52) hide show
  1. package/.planning/codebase/ARCHITECTURE.md +157 -0
  2. package/.planning/codebase/CONCERNS.md +145 -0
  3. package/.planning/codebase/CONVENTIONS.md +148 -0
  4. package/.planning/codebase/INTEGRATIONS.md +81 -0
  5. package/.planning/codebase/STACK.md +87 -0
  6. package/.planning/codebase/STRUCTURE.md +193 -0
  7. package/.planning/codebase/TESTING.md +243 -0
  8. package/esbuild.config.js +32 -3
  9. package/openclaw.plugin.json +1 -1
  10. package/package.json +2 -1
  11. package/scripts/compile-principles.mjs +94 -0
  12. package/scripts/sync-plugin.mjs +96 -281
  13. package/src/commands/pain.ts +12 -5
  14. package/src/commands/promote-impl.ts +13 -7
  15. package/src/commands/rollback.ts +10 -3
  16. package/src/core/event-log.ts +8 -6
  17. package/src/core/evolution-types.ts +33 -1
  18. package/src/core/principle-compiler/code-validator.ts +120 -0
  19. package/src/core/principle-compiler/compiler.ts +242 -0
  20. package/src/core/principle-compiler/index.ts +10 -0
  21. package/src/core/principle-compiler/ledger-registrar.ts +107 -0
  22. package/src/core/principle-compiler/template-generator.ts +108 -0
  23. package/src/core/reflection/reflection-context.ts +228 -0
  24. package/src/hooks/message-sanitize.ts +18 -5
  25. package/src/hooks/prompt.ts +15 -4
  26. package/src/hooks/subagent.ts +2 -3
  27. package/src/http/principles-console-route.ts +21 -4
  28. package/src/service/evolution-worker.ts +89 -365
  29. package/src/service/queue-io.ts +375 -0
  30. package/src/service/queue-migration.ts +122 -0
  31. package/src/service/sleep-cycle.ts +157 -0
  32. package/src/service/subagent-workflow/runtime-direct-driver.ts +1 -1
  33. package/src/service/workflow-watchdog.ts +168 -0
  34. package/src/tools/deep-reflect.ts +22 -11
  35. package/src/types/event-payload.ts +80 -0
  36. package/src/types/queue.ts +70 -0
  37. package/src/utils/file-lock.ts +2 -2
  38. package/src/utils/io.ts +11 -3
  39. package/tests/core/code-validator.test.ts +197 -0
  40. package/tests/core/evolution-migration.test.ts +325 -1
  41. package/tests/core/ledger-registrar.test.ts +232 -0
  42. package/tests/core/principle-compiler.test.ts +348 -0
  43. package/tests/core/queue-purge.test.ts +337 -0
  44. package/tests/core/reflection-context.test.ts +356 -0
  45. package/tests/core/template-generator.test.ts +101 -0
  46. package/tests/fixtures/legacy-queue-v1.json +74 -0
  47. package/tests/integration/principle-compiler-e2e.test.ts +335 -0
  48. package/tests/queue/async-lock.test.ts +200 -0
  49. package/tests/service/evolution-worker.queue.test.ts +296 -0
  50. package/tests/service/queue-io.test.ts +229 -0
  51. package/tests/service/queue-migration.test.ts +147 -0
  52. package/tests/service/workflow-watchdog.test.ts +372 -0
@@ -80,6 +80,14 @@ function formatEmpathyCard(stats: EmpathyEventStats, range: string, isZh: boolea
80
80
  return lines.join('\n');
81
81
  }
82
82
 
83
+ /**
84
+ * Extended context interface that includes sessionId injected by the plugin framework.
85
+ * PluginCommandContext does not include sessionId in its type definition.
86
+ */
87
+ interface SessionAwareCommandContext extends PluginCommandContext {
88
+ sessionId: string;
89
+ }
90
+
83
91
  /**
84
92
  * Handles the /pd-status command
85
93
  */
@@ -89,15 +97,14 @@ export function handlePainCommand(ctx: PluginCommandContext): PluginCommandResul
89
97
  const wctx = WorkspaceContext.fromHookContext({ workspaceDir, ...ctx.config });
90
98
  const lang = (ctx.config?.language as string) || 'en';
91
99
  const isZh = lang === 'zh';
92
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: sessionId injected by OpenClaw plugin framework - type not available in PluginCommandContext
93
- const {sessionId} = (ctx as any);
100
+ const { sessionId } = ctx as SessionAwareCommandContext;
94
101
 
95
102
  const args = (ctx.args || '').trim();
96
103
 
97
104
  // Handle empathy subcommand
98
105
  if (args.startsWith('empathy')) {
99
106
 
100
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
107
+
101
108
  return handleEmpathySubcommand(wctx, args, sessionId, isZh);
102
109
  }
103
110
 
@@ -138,7 +145,7 @@ export function handlePainCommand(ctx: PluginCommandContext): PluginCommandResul
138
145
 
139
146
  // Determine health status based on GFI
140
147
 
141
- // eslint-disable-next-line @typescript-eslint/init-declarations
148
+
142
149
  let healthLabel: string;
143
150
  let suggestionText = '';
144
151
 
@@ -218,7 +225,7 @@ export function handlePainCommand(ctx: PluginCommandContext): PluginCommandResul
218
225
  * Handle /pd-status empathy subcommand
219
226
  */
220
227
 
221
- // eslint-disable-next-line @typescript-eslint/max-params -- complexity 13, refactor candidate
228
+
222
229
  function handleEmpathySubcommand(
223
230
  wctx: WorkspaceContext,
224
231
  args: string,
@@ -28,7 +28,7 @@ import {
28
28
  } from '../core/principle-tree-ledger.js';
29
29
  import { WorkspaceContext } from '../core/workspace-context.js';
30
30
  import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
31
- import type { Implementation } from '../types/principle-tree-schema.js';
31
+ import type { Implementation, ImplementationLifecycleState } from '../types/principle-tree-schema.js';
32
32
  import { withLock } from '../utils/file-lock.js';
33
33
  import { atomicWriteFileSync } from '../utils/io.js';
34
34
 
@@ -37,16 +37,23 @@ function getAllImplementations(stateDir: string): Implementation[] {
37
37
  return Object.values(ledger.tree.implementations);
38
38
  }
39
39
 
40
+ /**
41
+ * Type predicate: true if impl has lifecycleState of 'candidate' or 'disabled'.
42
+ * The ledger adds lifecycleState at runtime beyond what's in the manifest interface.
43
+ */
44
+ function isCandidateOrDisabled(
45
+ impl: Implementation
46
+ ): impl is Implementation & { lifecycleState: ImplementationLifecycleState } {
47
+ return impl.lifecycleState === 'candidate' || impl.lifecycleState === 'disabled';
48
+ }
49
+
40
50
  function _handleListCandidates(
41
51
  stateDir: string,
42
52
  isZh: boolean,
43
53
  ): PluginCommandResult {
44
54
  const engine = new ReplayEngine('', stateDir);
45
55
  const allImpls = getAllImplementations(stateDir);
46
- const candidates = allImpls.filter(
47
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: lifecycleState is a dynamic property added by the system - type not in official interface
48
- (impl) => (impl as any).lifecycleState === 'candidate',
49
- );
56
+ const candidates = allImpls.filter(isCandidateOrDisabled);
50
57
 
51
58
  if (candidates.length === 0) {
52
59
  return {
@@ -141,8 +148,7 @@ function _handlePromoteImpl(options: PromoteImplOptions): PluginCommandResult {
141
148
  };
142
149
  }
143
150
 
144
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: lifecycleState is a dynamic property added by the system - type not in official interface
145
- const currentState = (candidate as any).lifecycleState || 'candidate';
151
+ const currentState = candidate.lifecycleState || 'candidate';
146
152
 
147
153
  if (currentState !== 'candidate' && currentState !== 'disabled') {
148
154
  return {
@@ -2,6 +2,14 @@ import { WorkspaceContext } from '../core/workspace-context.js';
2
2
  import { resetFriction } from '../core/session-tracker.js';
3
3
  import type { PluginCommandContext, PluginCommandResult } from '../openclaw-sdk.js';
4
4
 
5
+ /**
6
+ * Extended context interface that includes sessionId injected by the plugin framework.
7
+ * PluginCommandContext does not include sessionId in its type definition.
8
+ */
9
+ interface SessionAwareCommandContext extends PluginCommandContext {
10
+ sessionId: string;
11
+ }
12
+
5
13
  /**
6
14
  * Handles the /pd-rollback command
7
15
  *
@@ -15,8 +23,7 @@ export function handleRollbackCommand(ctx: PluginCommandContext): PluginCommandR
15
23
  const wctx = WorkspaceContext.fromHookContext({ workspaceDir, ...ctx.config });
16
24
  const lang = (ctx.config?.language as string) || 'en';
17
25
  const isZh = lang === 'zh';
18
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: sessionId injected by OpenClaw plugin framework - type not available in PluginCommandContext
19
- const {sessionId} = (ctx as any);
26
+ const { sessionId } = ctx as SessionAwareCommandContext;
20
27
 
21
28
  const args = (ctx.args || '').trim();
22
29
 
@@ -45,7 +52,7 @@ Usage:
45
52
  }
46
53
 
47
54
 
48
- // eslint-disable-next-line @typescript-eslint/init-declarations
55
+
49
56
  let eventId: string | null;
50
57
 
51
58
  const _triggerMethod = 'user_command' as const;
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  import * as fs from 'fs';
2
3
  import * as path from 'path';
3
4
  import type {
@@ -94,7 +95,7 @@ export class EventLog {
94
95
  /**
95
96
  * Clean up event files older than EVENT_LOG_RETENTION_DAYS.
96
97
  */
97
- private cleanupOldEventFiles(today: string): void {
98
+ private cleanupOldEventFiles(_today: string): void {
98
99
  if (EVENT_LOG_RETENTION_DAYS <= 0) return;
99
100
 
100
101
  try {
@@ -110,8 +111,8 @@ export class EventLog {
110
111
  fs.unlinkSync(filePath);
111
112
  }
112
113
  }
113
- } catch {
114
- // Silently fail cleanup
114
+ } catch (err) {
115
+ this.logger?.debug?.(`[PD] Event file cleanup failed (non-blocking): ${String(err)}`);
115
116
  }
116
117
  }
117
118
 
@@ -220,6 +221,7 @@ export class EventLog {
220
221
  }
221
222
  }
222
223
 
224
+ /* eslint-disable complexity */
223
225
  private updateStats(entry: EventLogEntry): void {
224
226
  let stats = this.statsCache.get(entry.date);
225
227
  if (!stats) {
@@ -228,8 +230,6 @@ export class EventLog {
228
230
  }
229
231
 
230
232
  if (entry.type === 'tool_call') {
231
-
232
- const _data = entry.data as unknown as ToolCallEventData;
233
233
  stats.tools.total++;
234
234
  if (entry.category === 'success') stats.tools.success++;
235
235
  else stats.tools.failure++;
@@ -349,7 +349,8 @@ export class EventLog {
349
349
  .map((line) => {
350
350
  try {
351
351
  return JSON.parse(line) as EventLogEntry;
352
- } catch {
352
+ } catch (err) {
353
+ this.logger?.warn?.(`[PD] Corrupted event line skipped: ${String(err).slice(0, 100)}`);
353
354
  return null;
354
355
  }
355
356
  })
@@ -499,6 +500,7 @@ export class EventLog {
499
500
  /**
500
501
  * Aggregate empathy stats for a specific session.
501
502
  */
503
+ /* eslint-disable complexity */
502
504
  private aggregateSessionEmpathy(sessionId: string, result: EmpathyEventStats): void {
503
505
  for (const entry of this.getMergedEvents()) {
504
506
  if (entry.sessionId === sessionId && entry.type === 'pain_signal') {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Evolution Points System V2.0 - MVP
3
- *
3
+ *
4
4
  * Core Philosophy: Growth-driven替代Penalty-driven
5
5
  * - 起点0分,只能增加,不扣分
6
6
  * - 失败记录教训,不扣分
@@ -8,6 +8,9 @@
8
8
  * - 5级成长路径:Seed → Forest
9
9
  */
10
10
 
11
+ // V2 queue types require TaskKind/TaskPriority from trajectory-types
12
+ import type { TaskKind, TaskPriority } from './trajectory-types.js';
13
+
11
14
  // ===== 等级定义 =====
12
15
 
13
16
 
@@ -464,3 +467,32 @@ export type EvolutionLoopEvent =
464
467
  | { ts: string; type: 'principle_rolled_back'; data: PrincipleRolledBackData }
465
468
  | { ts: string; type: 'circuit_breaker_opened'; data: CircuitBreakerOpenedData }
466
469
  | { ts: string; type: 'legacy_import'; data: LegacyImportData };
470
+
471
+ // V2 Queue Types (moved from evolution-worker.ts for shared use)
472
+ export type QueueStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'canceled';
473
+ export type TaskResolution = 'marker_detected' | 'auto_completed_timeout' | 'failed_max_retries' | 'runtime_unavailable' | 'canceled' | 'late_marker_principle_created' | 'late_marker_no_principle' | 'stub_fallback' | 'skipped_thin_violation' | 'success' | 'failure' | 'skipped';
474
+
475
+ export interface EvolutionQueueItem {
476
+ id: string;
477
+ taskKind: TaskKind;
478
+ priority: TaskPriority;
479
+ source: string;
480
+ traceId?: string;
481
+ task?: string;
482
+ score: number;
483
+ reason: string;
484
+ timestamp: string;
485
+ enqueued_at?: string;
486
+ started_at?: string;
487
+ completed_at?: string;
488
+ assigned_session_key?: string;
489
+ trigger_text_preview?: string;
490
+ status: QueueStatus;
491
+ resolution?: TaskResolution;
492
+ session_id?: string;
493
+ agent_id?: string;
494
+ retryCount: number;
495
+ maxRetries: number;
496
+ lastError?: string;
497
+ resultRef?: string;
498
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Code Validator — Validates LLM-generated rule implementation code
3
+ *
4
+ * PURPOSE: Ensure generated code is safe, syntactically correct, and exports
5
+ * the expected shape before it is stored as a rule implementation.
6
+ *
7
+ * CHECKS:
8
+ * 1. Syntax: code parses without errors
9
+ * 2. Forbidden patterns: no require, import, fetch, eval, Function, process, globalThis
10
+ * 3. Export check: sandbox loads and exports evaluate + meta
11
+ * 4. Return shape: evaluate(mockInput) returns { matched: boolean }
12
+ *
13
+ * Reuses loadRuleImplementationModule for sandbox execution (node:vm isolation).
14
+ */
15
+
16
+ import { nodeVm } from '../../utils/node-vm-polyfill.js';
17
+ import { loadRuleImplementationModule } from '../rule-implementation-runtime.js';
18
+
19
+ export interface ValidationResult {
20
+ valid: boolean;
21
+ errors: string[];
22
+ warnings: string[];
23
+ }
24
+
25
+ const FORBIDDEN_PATTERNS: { pattern: RegExp; label: string }[] = [
26
+ { pattern: /\brequire\s*\(/, label: 'require' },
27
+ { pattern: /\bimport\s+/, label: 'import' },
28
+ { pattern: /\bfetch\s*\(/, label: 'fetch' },
29
+ { pattern: /\beval\s*\(/, label: 'eval' },
30
+ { pattern: /\bFunction\s*\(/, label: 'Function' },
31
+ { pattern: /\bprocess\b/, label: 'process' },
32
+ { pattern: /\bglobalThis\b/, label: 'globalThis' },
33
+ { pattern: /\bglobal\b/, label: 'global' },
34
+ { pattern: /\bReflect\b/, label: 'Reflect' },
35
+ { pattern: /\bProxy\b/, label: 'Proxy' },
36
+ { pattern: /\bconstructor\b/, label: 'constructor' },
37
+ { pattern: /\bBuffer\b/, label: 'Buffer' },
38
+ { pattern: /\bsetTimeout\b/, label: 'setTimeout' },
39
+ { pattern: /\bsetInterval\b/, label: 'setInterval' },
40
+ // Bracket notation access to globals
41
+ { pattern: /\[\s*['"](require|import|fetch|eval|process|globalThis|global|Reflect|Proxy|Buffer|Function)\s*['"]\s*\]/, label: 'bracket access to forbidden global' },
42
+ ];
43
+
44
+ const MOCK_INPUT = {
45
+ action: {
46
+ toolName: 'bash',
47
+ normalizedPath: '/tmp/test.ts',
48
+ paramsSummary: { command: 'echo test' },
49
+ },
50
+ workspace: { isRiskPath: false, planStatus: 'NONE', hasPlanFile: false },
51
+ session: { sessionId: 'test', currentGfi: 0, recentThinking: false },
52
+ evolution: { epTier: 0 },
53
+ derived: { estimatedLineChanges: 0, bashRisk: 'safe' },
54
+ };
55
+
56
+ export function validateGeneratedCode(code: string): ValidationResult {
57
+ const errors: string[] = [];
58
+ const warnings: string[] = [];
59
+
60
+ // --- Check 1: Syntax ---
61
+ // Normalize export keywords so vm.Script can parse ES module source
62
+ const normalized = code
63
+ .replace(/export\s+const\s+/g, 'const ')
64
+ .replace(/export\s+function\s+/g, 'function ');
65
+ try {
66
+ new nodeVm.Script(normalized, { filename: 'code-validator-syntax.js' });
67
+ } catch (err) {
68
+ errors.push(`Syntax error: ${(err as Error).message}`);
69
+ return { valid: false, errors, warnings };
70
+ }
71
+
72
+ // --- Check 2: Forbidden patterns ---
73
+ for (const { pattern, label } of FORBIDDEN_PATTERNS) {
74
+ if (pattern.test(code)) {
75
+ errors.push(`Forbidden pattern: ${label}`);
76
+ }
77
+ }
78
+
79
+ if (errors.length > 0) {
80
+ return { valid: false, errors, warnings };
81
+ }
82
+
83
+ // --- Check 3: Sandbox load + export check ---
84
+ let moduleExports: { meta?: unknown; evaluate?: unknown };
85
+ try {
86
+ moduleExports = loadRuleImplementationModule(code, 'code-validator-candidate.js');
87
+ } catch (err) {
88
+ errors.push(`Sandbox compilation error: ${(err as Error).message}`);
89
+ return { valid: false, errors, warnings };
90
+ }
91
+
92
+ if (!moduleExports.meta || typeof moduleExports.meta !== 'object') {
93
+ errors.push('Missing export: meta');
94
+ }
95
+
96
+ if (typeof moduleExports.evaluate !== 'function') {
97
+ errors.push('Missing export: evaluate');
98
+ }
99
+
100
+ if (errors.length > 0) {
101
+ return { valid: false, errors, warnings };
102
+ }
103
+
104
+ // --- Check 4: Return shape ---
105
+ try {
106
+ const result = (moduleExports.evaluate as (input: unknown) => unknown)(MOCK_INPUT);
107
+ if (!result || typeof result !== 'object') {
108
+ errors.push('evaluate must return an object');
109
+ } else if (typeof (result as Record<string, unknown>).matched !== 'boolean') {
110
+ errors.push('evaluate must return { matched: boolean }');
111
+ }
112
+ } catch (evalWarning) {
113
+ // evaluate throwing on mock input is acceptable — the function exists and
114
+ // has the right signature, it just can't handle our generic mock data.
115
+ // Track as a non-blocking warning so operators know the rule may be fragile.
116
+ warnings.push(`evaluate() threw on mock input: ${(evalWarning as Error).message}`);
117
+ }
118
+
119
+ return { valid: errors.length === 0, errors, warnings };
120
+ }
@@ -0,0 +1,242 @@
1
+ /**
2
+ * PrincipleCompiler — Orchestrator (Task 5)
3
+ *
4
+ * Orchestrates the full compilation flow:
5
+ * ReflectionContextCollector.collect() → extract patterns → generateFromTemplate()
6
+ * → validateGeneratedCode() → registerCompiledRule()
7
+ *
8
+ * DESIGN DECISIONS:
9
+ * - extractPatterns infers toolName from pain event reasons and session tool calls
10
+ * - Groups by toolName into PainPattern objects
11
+ * - If no patterns can be extracted, returns a 'no patterns' failure
12
+ */
13
+
14
+ import { ReflectionContextCollector } from '../reflection/reflection-context.js';
15
+ import { validateGeneratedCode } from './code-validator.js';
16
+ import { generateFromTemplate, type PainPattern } from './template-generator.js';
17
+ import { registerCompiledRule } from './ledger-registrar.js';
18
+ import { createImplementationAssetDir } from '../code-implementation-storage.js';
19
+ import type { TrajectoryDatabase } from '../trajectory.js';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Types
23
+ // ---------------------------------------------------------------------------
24
+
25
+ export interface CompileResult {
26
+ success: boolean;
27
+ principleId: string;
28
+ ruleId?: string;
29
+ implementationId?: string;
30
+ code?: string;
31
+ reason?: string;
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Constants
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /** Tool names to look for when scanning text for tool references */
39
+ const KNOWN_TOOLS = ['bash', 'write', 'edit', 'read', 'grep', 'glob', 'mcp'] as const;
40
+
41
+ /** Regex to extract file paths from reason text */
42
+ const PATH_REGEX = /(?:\/[\w.-]+){2,}/;
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Pattern Extraction
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /**
49
+ * Extract PainPatterns from a ReflectionContext.
50
+ *
51
+ * Strategy:
52
+ * 1. Scan pain event reasons for known tool names
53
+ * 2. Extract file paths from reason text as pathRegex candidates
54
+ * 3. Cross-reference with sessionSnapshot toolCalls for failed tool calls
55
+ * 4. Group by toolName into PainPattern objects
56
+ */
57
+ function extractPatterns(context: {
58
+ painEvents: Array<{ reason: string | null; source: string }>;
59
+ sessionSnapshot: {
60
+ toolCalls: Array<{
61
+ toolName: string;
62
+ outcome: string;
63
+ filePath: string | null;
64
+ errorType: string | null;
65
+ }>;
66
+ } | null;
67
+ }): PainPattern[] {
68
+ const toolNameMap = new Map<string, PainPattern>();
69
+
70
+ // 1. Extract from pain event reasons
71
+ for (const pe of context.painEvents) {
72
+ const text = pe.reason ?? pe.source ?? '';
73
+ const toolName = inferToolName(text);
74
+ if (!toolName) continue;
75
+
76
+ const pathRegex = extractPathRegex(text);
77
+
78
+ if (!toolNameMap.has(toolName)) {
79
+ toolNameMap.set(toolName, { toolName });
80
+ }
81
+
82
+ const pattern = toolNameMap.get(toolName)!;
83
+ if (pathRegex && !pattern.pathRegex) {
84
+ pattern.pathRegex = pathRegex;
85
+ }
86
+ }
87
+
88
+ // 2. Extract from session snapshot tool calls (failed ones)
89
+ if (context.sessionSnapshot?.toolCalls) {
90
+ for (const tc of context.sessionSnapshot.toolCalls) {
91
+ // Focus on failed/blocked tool calls as they indicate pain
92
+ if (tc.outcome !== 'failure' && tc.outcome !== 'blocked') continue;
93
+
94
+ const toolName = tc.toolName;
95
+ if (!toolNameMap.has(toolName)) {
96
+ const pattern: PainPattern = { toolName };
97
+ if (tc.errorType) {
98
+ pattern.errorType = tc.errorType;
99
+ }
100
+ if (tc.filePath) {
101
+ pattern.pathRegex = escapeRegex(tc.filePath);
102
+ }
103
+ toolNameMap.set(toolName, pattern);
104
+ } else {
105
+ const existing = toolNameMap.get(toolName)!;
106
+ if (tc.errorType && !existing.errorType) {
107
+ existing.errorType = tc.errorType;
108
+ }
109
+ if (tc.filePath && !existing.pathRegex) {
110
+ existing.pathRegex = escapeRegex(tc.filePath);
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ return Array.from(toolNameMap.values());
117
+ }
118
+
119
+ /**
120
+ * Infer tool name from text by checking for known tool names.
121
+ * Returns the first matching known tool name, or null if none found.
122
+ */
123
+ function inferToolName(text: string): string | null {
124
+ const lower = text.toLowerCase();
125
+ for (const tool of KNOWN_TOOLS) {
126
+ // Match as a standalone word to avoid false positives
127
+ // e.g., "bash" in "bash" or "bash command" but not in "ambush"
128
+ const regex = new RegExp(`\\b${tool}\\b`);
129
+ if (regex.test(lower)) {
130
+ return tool;
131
+ }
132
+ }
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * Extract a file path from text and return it as an escaped regex pattern.
138
+ * Returns the first path found, or null.
139
+ */
140
+ function extractPathRegex(text: string): string | null {
141
+ const match = PATH_REGEX.exec(text);
142
+ if (match) {
143
+ return escapeRegex(match[0]);
144
+ }
145
+ return null;
146
+ }
147
+
148
+ /**
149
+ * Escape special regex characters in a string.
150
+ */
151
+ function escapeRegex(str: string): string {
152
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // PrincipleCompiler
157
+ // ---------------------------------------------------------------------------
158
+
159
+ export class PrincipleCompiler {
160
+ private readonly stateDir: string;
161
+ private readonly collector: ReflectionContextCollector;
162
+
163
+ constructor(stateDir: string, trajectory: TrajectoryDatabase) {
164
+ this.stateDir = stateDir;
165
+ this.collector = new ReflectionContextCollector(stateDir, trajectory);
166
+ }
167
+
168
+ /**
169
+ * Compile a single principle into an auto-generated rule.
170
+ *
171
+ * Flow:
172
+ * 1. Collect reflection context
173
+ * 2. Extract pain patterns
174
+ * 3. Generate code from template
175
+ * 4. Validate generated code
176
+ * 5. Register in ledger
177
+ */
178
+ compileOne(principleId: string): CompileResult {
179
+ // Step 1: Collect context
180
+ const context = this.collector.collect(principleId);
181
+ if (!context) {
182
+ return { success: false, principleId, reason: 'no context' };
183
+ }
184
+
185
+ // Step 2: Extract patterns
186
+ const patterns = extractPatterns({
187
+ painEvents: context.painEvents,
188
+ sessionSnapshot: context.sessionSnapshot,
189
+ });
190
+
191
+ // Step 3: Generate code
192
+ const coversCondition = context.principle.triggerPattern || context.principle.text;
193
+ const code = generateFromTemplate(principleId, coversCondition, patterns);
194
+ if (!code) {
195
+ return { success: false, principleId, reason: 'no patterns' };
196
+ }
197
+
198
+ // Step 4: Validate
199
+ const validation = validateGeneratedCode(code);
200
+ if (!validation.valid) {
201
+ return {
202
+ success: false,
203
+ principleId,
204
+ reason: `validation failed: ${validation.errors.join('; ')}`,
205
+ };
206
+ }
207
+
208
+ // Step 5: Register
209
+ const registration = registerCompiledRule(this.stateDir, {
210
+ principleId,
211
+ codeContent: code,
212
+ coversCondition,
213
+ });
214
+
215
+ // Step 6: Persist code to disk so RuleHost can load it
216
+ createImplementationAssetDir(this.stateDir, registration.implementationId, '1', {
217
+ entrySource: code,
218
+ });
219
+
220
+ return {
221
+ success: true,
222
+ principleId,
223
+ ruleId: registration.ruleId,
224
+ implementationId: registration.implementationId,
225
+ code,
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Compile all eligible principles (those with derivedFromPainIds).
231
+ */
232
+ compileAll(): CompileResult[] {
233
+ const contexts = this.collector.collectBatch();
234
+ return contexts.map((ctx) => {
235
+ try {
236
+ return this.compileOne(ctx.principle.id);
237
+ } catch (e) {
238
+ return { success: false, principleId: ctx.principle.id, reason: `unhandled: ${(e as Error).message}` };
239
+ }
240
+ });
241
+ }
242
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Principle Compiler — Barrel Export
3
+ *
4
+ * Re-exports all principle-compiler components for convenient importing.
5
+ */
6
+
7
+ export { PrincipleCompiler, type CompileResult } from './compiler.js';
8
+ export { validateGeneratedCode, type ValidationResult } from './code-validator.js';
9
+ export { generateFromTemplate, type PainPattern } from './template-generator.js';
10
+ export { registerCompiledRule, type RegisterInput, type RegisterResult } from './ledger-registrar.js';