principles-disciple 1.139.0 → 1.141.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.
@@ -1,24 +1,38 @@
1
+ /**
2
+ * Detection Funnel — PRI-446 thin adapter
3
+ *
4
+ * The pure three-layer funnel logic (LRU cache, async queue, layer dispatch)
5
+ * now lives in principles-core
6
+ * (runtime-v2/detection/detection-funnel-policy.ts). This file is a thin shell
7
+ * that wires DetectionFunnelCore with the two environment-coupled dependencies
8
+ * the plugin owns:
9
+ * - crypto.createHash (the L2 cache key hasher)
10
+ * - PainDictionary.match (the L1 exact matcher)
11
+ * - shouldIgnorePainProtocolText (the protocol-token gate)
12
+ *
13
+ * It re-exports DetectionFunnel (same class name + constructor signature) and
14
+ * DetectionResult so DetectionService, llm.ts, and evolution-worker keep working
15
+ * unchanged.
16
+ *
17
+ * ERR checklist:
18
+ * - ERR-011: this is a thin adapter delegating to core pure logic.
19
+ */
1
20
  import { type PainDictionary } from './dictionary.js';
2
- export interface DetectionResult {
3
- detected: boolean;
4
- severity?: number;
5
- ruleId?: string;
6
- source: 'l1_exact' | 'l2_cache' | 'l3_async_queued' | 'l3_semantic_hit';
7
- }
21
+ import { type DetectionResult } from '@principles/core/runtime-v2';
22
+ export type { DetectionResult };
8
23
  /**
9
24
  * Orchestrates the three-layer detection funnel for pain signals.
25
+ *
26
+ * Delegates to the core DetectionFunnelCore, injecting the crypto hasher,
27
+ * the dictionary matcher, and the protocol-token gate.
10
28
  */
11
29
  export declare class DetectionFunnel {
12
- private readonly cache;
13
- private asyncQueue;
14
- private readonly dictionary;
30
+ private readonly core;
15
31
  constructor(dictionary: PainDictionary);
16
32
  /**
17
33
  * Detects pain in the given text using L1 (Exact), L2 (Cache), and L3 (Async).
18
34
  */
19
35
  detect(text: string): DetectionResult;
20
- private static computeHash;
21
- private enqueueAsync;
22
36
  /**
23
37
  * Internal method for the worker to update the cache after a semantic hit.
24
38
  */
@@ -1,104 +1,56 @@
1
- import { createHash } from 'crypto';
2
- import { shouldIgnorePainProtocolText } from './dictionary.js';
3
1
  /**
4
- * A simple LRU Cache implementation using Map.
2
+ * Detection Funnel PRI-446 thin adapter
3
+ *
4
+ * The pure three-layer funnel logic (LRU cache, async queue, layer dispatch)
5
+ * now lives in principles-core
6
+ * (runtime-v2/detection/detection-funnel-policy.ts). This file is a thin shell
7
+ * that wires DetectionFunnelCore with the two environment-coupled dependencies
8
+ * the plugin owns:
9
+ * - crypto.createHash (the L2 cache key hasher)
10
+ * - PainDictionary.match (the L1 exact matcher)
11
+ * - shouldIgnorePainProtocolText (the protocol-token gate)
12
+ *
13
+ * It re-exports DetectionFunnel (same class name + constructor signature) and
14
+ * DetectionResult so DetectionService, llm.ts, and evolution-worker keep working
15
+ * unchanged.
16
+ *
17
+ * ERR checklist:
18
+ * - ERR-011: this is a thin adapter delegating to core pure logic.
5
19
  */
6
- class SimpleLRU {
7
- cache;
8
- maxSize;
9
- constructor(maxSize = 100) {
10
- this.cache = new Map();
11
- this.maxSize = maxSize;
12
- }
13
- get(key) {
14
- const item = this.cache.get(key);
15
- if (item !== undefined) {
16
- // Refresh: delete and re-insert
17
- this.cache.delete(key);
18
- this.cache.set(key, item);
19
- }
20
- return item;
21
- }
22
- set(key, value) {
23
- if (this.cache.has(key)) {
24
- this.cache.delete(key);
25
- }
26
- else if (this.cache.size >= this.maxSize) {
27
- // Remove the oldest (first) item
28
- const firstKey = this.cache.keys().next().value;
29
- if (firstKey !== undefined) {
30
- this.cache.delete(firstKey);
31
- }
32
- }
33
- this.cache.set(key, value);
34
- }
35
- }
20
+ import { createHash } from 'crypto';
21
+ import { shouldIgnorePainProtocolText } from './dictionary.js';
22
+ import { DetectionFunnelCore, } from '@principles/core/runtime-v2';
36
23
  /**
37
24
  * Orchestrates the three-layer detection funnel for pain signals.
25
+ *
26
+ * Delegates to the core DetectionFunnelCore, injecting the crypto hasher,
27
+ * the dictionary matcher, and the protocol-token gate.
38
28
  */
39
29
  export class DetectionFunnel {
40
- cache = new SimpleLRU(100);
41
- asyncQueue = [];
42
- dictionary;
30
+ core;
43
31
  constructor(dictionary) {
44
- this.dictionary = dictionary;
32
+ this.core = new DetectionFunnelCore({
33
+ match: (text) => dictionary.match(text),
34
+ hash: (text) => createHash('sha256').update(text).digest('hex'),
35
+ shouldIgnoreProtocol: shouldIgnorePainProtocolText,
36
+ });
45
37
  }
46
38
  /**
47
39
  * Detects pain in the given text using L1 (Exact), L2 (Cache), and L3 (Async).
48
40
  */
49
41
  detect(text) {
50
- if (shouldIgnorePainProtocolText(text)) {
51
- return { detected: false, source: 'l1_exact' };
52
- }
53
- // --- Layer 1: Exact Match (Sync) ---
54
- const exactMatch = this.dictionary.match(text);
55
- if (exactMatch) {
56
- return {
57
- detected: true,
58
- severity: exactMatch.severity,
59
- ruleId: exactMatch.ruleId,
60
- source: 'l1_exact'
61
- };
62
- }
63
- // --- Layer 2: LRU Cache (Sync) ---
64
- const hash = DetectionFunnel.computeHash(text);
65
- const cached = this.cache.get(hash);
66
- if (cached) {
67
- return {
68
- detected: cached.detected,
69
- severity: cached.severity,
70
- source: 'l2_cache'
71
- };
72
- }
73
- // --- Layer 3: Async Semantic Queue ---
74
- this.enqueueAsync(text);
75
- return {
76
- detected: false,
77
- source: 'l3_async_queued'
78
- };
79
- }
80
- static computeHash(text) {
81
- return createHash('sha256').update(text).digest('hex');
82
- }
83
- enqueueAsync(text) {
84
- if (this.asyncQueue.length < 1000) {
85
- this.asyncQueue.push(text);
86
- }
87
- // Worker will pick this up and perform semantic search via createMemorySearchTool
42
+ return this.core.detect(text);
88
43
  }
89
44
  /**
90
45
  * Internal method for the worker to update the cache after a semantic hit.
91
46
  */
92
47
  updateCache(text, result) {
93
- const hash = DetectionFunnel.computeHash(text);
94
- this.cache.set(hash, result);
48
+ this.core.updateCache(text, result);
95
49
  }
96
50
  /**
97
51
  * Retrieves and clears the current asynchronous queue.
98
52
  */
99
53
  flushQueue() {
100
- const queue = [...this.asyncQueue];
101
- this.asyncQueue = [];
102
- return queue;
54
+ return this.core.flushQueue();
103
55
  }
104
56
  }
@@ -1,30 +1,24 @@
1
- declare const PAIN_DIAGNOSTIC_SOURCES: readonly ["manual", "tool_failure", "dispatch_error", "gate_blocked", "user_empathy", "llm_paralysis", "semantic", "subagent_error"];
2
- export type PainDiagnosticSource = typeof PAIN_DIAGNOSTIC_SOURCES[number];
3
- export type PainDiagnosticGateReason = 'manual' | 'high_gfi' | 'repeated_failure' | 'semantic_pain' | 'llm_paralysis' | 'risky_high_score' | 'subagent_error' | 'gate_blocked' | 'cooldown' | 'below_gate';
4
- export interface PainDiagnosticGateInput {
5
- source: PainDiagnosticSource | string;
6
- score: number;
7
- currentGfi: number;
8
- consecutiveErrors?: number;
9
- isRisky?: boolean;
10
- errorHash?: string;
11
- sessionId?: string;
12
- nowMs?: number;
13
- cooldownMs?: number;
14
- thresholds?: {
15
- painTrigger?: number;
16
- highSeverity?: number;
17
- highGfi?: number;
18
- repeatedFailure?: number;
19
- semanticPain?: number;
20
- };
21
- }
22
- export interface PainDiagnosticGateDecision {
23
- shouldDiagnose: boolean;
24
- reason: PainDiagnosticGateReason;
25
- episodeKey: string;
26
- detail: string;
27
- }
1
+ /**
2
+ * Pain Diagnostic Gate — PRI-446 thin adapter
3
+ *
4
+ * The pure decision logic (threshold tree, cooldown comparison, episode-key
5
+ * construction) now lives in principles-core
6
+ * (runtime-v2/pain-gate/pain-diagnostic-gate-policy.ts). This file is the
7
+ * stateful adapter that owns:
8
+ * - the cooldown Map (lastDiagnosedAtByEpisode)
9
+ * - Date.now() injection
10
+ * - SystemLogger for unknown-source telemetry
11
+ *
12
+ * It preserves the original export names (evaluatePainDiagnosticGate,
13
+ * isCooldownActiveForEpisode, resetPainDiagnosticGateForTest) so all 5 callers
14
+ * (gate-block-helper, llm, pain, prompt x2) and the characterization test keep
15
+ * working unchanged.
16
+ *
17
+ * ERR checklist:
18
+ * - ERR-011: this is a stateful adapter delegating to core pure logic.
19
+ */
20
+ import { type PainDiagnosticSource, type PainDiagnosticGateReason, type PainDiagnosticGateInput, type PainDiagnosticGateDecision } from '@principles/core/runtime-v2';
21
+ export type { PainDiagnosticSource, PainDiagnosticGateReason, PainDiagnosticGateInput, PainDiagnosticGateDecision, };
28
22
  export declare function resetPainDiagnosticGateForTest(): void;
29
23
  /**
30
24
  * Check whether cooldown is currently active for a given episode.
@@ -32,5 +26,9 @@ export declare function resetPainDiagnosticGateForTest(): void;
32
26
  * with the PainDiagnosticGate's cooldown state.
33
27
  */
34
28
  export declare function isCooldownActiveForEpisode(source: string, sessionId: string | undefined, errorHash: string | undefined, cooldownMs?: number): boolean;
29
+ /**
30
+ * Evaluate the pain diagnostic gate. Delegates the pure decision to core and,
31
+ * when the decision is to diagnose, records the current time against the
32
+ * episode so subsequent calls within the cooldown window are suppressed.
33
+ */
35
34
  export declare function evaluatePainDiagnosticGate(input: PainDiagnosticGateInput): PainDiagnosticGateDecision;
36
- export {};
@@ -1,42 +1,26 @@
1
+ /**
2
+ * Pain Diagnostic Gate — PRI-446 thin adapter
3
+ *
4
+ * The pure decision logic (threshold tree, cooldown comparison, episode-key
5
+ * construction) now lives in principles-core
6
+ * (runtime-v2/pain-gate/pain-diagnostic-gate-policy.ts). This file is the
7
+ * stateful adapter that owns:
8
+ * - the cooldown Map (lastDiagnosedAtByEpisode)
9
+ * - Date.now() injection
10
+ * - SystemLogger for unknown-source telemetry
11
+ *
12
+ * It preserves the original export names (evaluatePainDiagnosticGate,
13
+ * isCooldownActiveForEpisode, resetPainDiagnosticGateForTest) so all 5 callers
14
+ * (gate-block-helper, llm, pain, prompt x2) and the characterization test keep
15
+ * working unchanged.
16
+ *
17
+ * ERR checklist:
18
+ * - ERR-011: this is a stateful adapter delegating to core pure logic.
19
+ */
1
20
  import { SystemLogger } from './system-logger.js';
2
- const PAIN_DIAGNOSTIC_SOURCES = [
3
- 'manual',
4
- 'tool_failure',
5
- 'dispatch_error',
6
- 'gate_blocked',
7
- 'user_empathy',
8
- 'llm_paralysis',
9
- 'semantic',
10
- 'subagent_error',
11
- ];
12
- const DEFAULT_COOLDOWN_MS = 15 * 60 * 1000;
21
+ import { evaluatePainDiagnosticGateDecision, normalizedSource, buildEpisodeKey, isCooldownActive as isCooldownActiveCore, } from '@principles/core/runtime-v2';
22
+ // Module-level cooldown state — owned by this adapter (core is stateless).
13
23
  const lastDiagnosedAtByEpisode = new Map();
14
- function normalizedSource(source) {
15
- if (source.startsWith('llm_') && source !== 'llm_paralysis') {
16
- return 'semantic';
17
- }
18
- if (!PAIN_DIAGNOSTIC_SOURCES.includes(source)) {
19
- SystemLogger.log('', 'GATE_UNKNOWN_SOURCE', `Unknown pain source: "${source}"`);
20
- }
21
- return source;
22
- }
23
- function buildEpisodeKey(input) {
24
- const source = normalizedSource(input.source);
25
- const sessionId = input.sessionId || 'unknown';
26
- const hash = input.errorHash || 'no-hash';
27
- return `${sessionId}:${source}:${hash}`;
28
- }
29
- function withinCooldown(input, episodeKey) {
30
- const cooldownMs = input.cooldownMs ?? DEFAULT_COOLDOWN_MS;
31
- if (cooldownMs <= 0)
32
- return false;
33
- const nowMs = input.nowMs ?? Date.now();
34
- const last = lastDiagnosedAtByEpisode.get(episodeKey);
35
- return last !== undefined && nowMs - last < cooldownMs;
36
- }
37
- function markDiagnosed(input, episodeKey) {
38
- lastDiagnosedAtByEpisode.set(episodeKey, input.nowMs ?? Date.now());
39
- }
40
24
  export function resetPainDiagnosticGateForTest() {
41
25
  lastDiagnosedAtByEpisode.clear();
42
26
  }
@@ -46,60 +30,29 @@ export function resetPainDiagnosticGateForTest() {
46
30
  * with the PainDiagnosticGate's cooldown state.
47
31
  */
48
32
  export function isCooldownActiveForEpisode(source, sessionId, errorHash, cooldownMs) {
49
- const episodeKey = buildEpisodeKey({ source, sessionId, errorHash });
50
- return withinCooldown({ source, sessionId, errorHash, cooldownMs }, episodeKey);
33
+ const episodeKey = buildEpisodeKey({ source, sessionId, errorHash, score: 0, currentGfi: 0 });
34
+ const last = lastDiagnosedAtByEpisode.get(episodeKey);
35
+ const nowMs = Date.now();
36
+ return isCooldownActiveCore({ source, sessionId, errorHash, cooldownMs, nowMs, lastDiagnosedAtMs: last });
51
37
  }
38
+ /**
39
+ * Evaluate the pain diagnostic gate. Delegates the pure decision to core and,
40
+ * when the decision is to diagnose, records the current time against the
41
+ * episode so subsequent calls within the cooldown window are suppressed.
42
+ */
52
43
  export function evaluatePainDiagnosticGate(input) {
53
- const source = normalizedSource(input.source);
54
- const episodeKey = buildEpisodeKey(input);
55
- const painTrigger = input.thresholds?.painTrigger ?? 40;
56
- const highSeverity = input.thresholds?.highSeverity ?? 70;
57
- const highGfi = input.thresholds?.highGfi ?? Math.max(highSeverity, painTrigger + 30);
58
- const repeatedFailure = input.thresholds?.repeatedFailure ?? 4;
59
- const semanticPain = input.thresholds?.semanticPain ?? Math.max(painTrigger, 60);
60
- const score = Number.isFinite(input.score) ? input.score : 0;
61
- const currentGfi = Number.isFinite(input.currentGfi) ? input.currentGfi : 0;
62
- const consecutiveErrors = Number.isFinite(input.consecutiveErrors) ? input.consecutiveErrors : 0;
63
- const approve = (reason, detail) => {
64
- if (withinCooldown(input, episodeKey)) {
65
- return {
66
- shouldDiagnose: false,
67
- reason: 'cooldown',
68
- episodeKey,
69
- detail: `recently diagnosed; ${detail}`,
70
- };
71
- }
72
- markDiagnosed(input, episodeKey);
73
- return { shouldDiagnose: true, reason, episodeKey, detail };
74
- };
75
- if (source === 'manual') {
76
- return approve('manual', 'manual pain signal bypasses automatic gate');
77
- }
78
- if (source === 'subagent_error' && score >= painTrigger) {
79
- return approve('subagent_error', `subagent error score ${score} >= ${painTrigger}`);
80
- }
81
- if (source === 'llm_paralysis' && score >= painTrigger) {
82
- return approve('llm_paralysis', `llm paralysis score ${score} >= ${painTrigger}`);
83
- }
84
- if (source === 'gate_blocked' && score >= painTrigger) {
85
- return approve('gate_blocked', `gate blocked score ${score} >= ${painTrigger}`);
86
- }
87
- if ((source === 'user_empathy' || source === 'semantic') && score >= semanticPain) {
88
- return approve('semantic_pain', `semantic pain score ${score} >= ${semanticPain}`);
44
+ // Surface unknown sources via telemetry (core cannot log).
45
+ const { unknown } = normalizedSource(input.source);
46
+ if (unknown) {
47
+ SystemLogger.log('', 'GATE_UNKNOWN_SOURCE', `Unknown pain source: "${input.source}"`);
89
48
  }
90
- if (input.isRisky === true && score >= highSeverity) {
91
- return approve('risky_high_score', `risky operation score ${score} >= ${highSeverity}`);
92
- }
93
- if (consecutiveErrors >= repeatedFailure) {
94
- return approve('repeated_failure', `consecutive errors ${consecutiveErrors} >= ${repeatedFailure}`);
95
- }
96
- if (currentGfi >= highGfi) {
97
- return approve('high_gfi', `GFI ${currentGfi.toFixed(1)} >= ${highGfi}`);
49
+ const episodeKey = buildEpisodeKey(input);
50
+ const last = lastDiagnosedAtByEpisode.get(episodeKey);
51
+ const nowMs = input.nowMs ?? Date.now();
52
+ const decision = evaluatePainDiagnosticGateDecision({ ...input, nowMs }, last);
53
+ // Record the diagnosis time when approved (matches prior markDiagnosed behavior).
54
+ if (decision.shouldDiagnose) {
55
+ lastDiagnosedAtByEpisode.set(episodeKey, nowMs);
98
56
  }
99
- return {
100
- shouldDiagnose: false,
101
- reason: 'below_gate',
102
- episodeKey,
103
- detail: `score=${score}; gfi=${currentGfi.toFixed(1)}; consecutive=${consecutiveErrors}`,
104
- };
57
+ return decision;
105
58
  }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Pure-logic helpers for prompt assembly.
3
+ *
4
+ * Extracted from hooks/prompt.ts per PRI-444. These functions contain NO I/O
5
+ * and NO side effects — they are independently unit-testable.
6
+ *
7
+ * I/O helpers (cachedReadFile, loadContextInjectionConfig, resolveEmpathyObserver)
8
+ * remain in prompt.ts because they depend on module-level cache state and fs.
9
+ *
10
+ * Pattern follows after-tool-call-helpers.ts (PRI-326): plugin-internal
11
+ * decomposition + core pure-function reuse.
12
+ *
13
+ * ERR checklist:
14
+ * EP-01: All unknown inputs use typeof/Object.hasOwn guards, never `as`
15
+ * EP-03: Pure functions never swallow errors; invalid input returns empty string
16
+ * EP-09: Pure functions are independently unit-testable without mocks
17
+ */
18
+ import type { ExtractedUserMessage, CorePrincipleEntry, EvolutionPrincipleEntry, AppendSystemContextParts } from './prompt-types.js';
19
+ /**
20
+ * Extract the actual user message from the raw prompt text.
21
+ *
22
+ * The prompt may contain:
23
+ * - Boot check messages (system-generated, return empty)
24
+ * - Feishu wrapper format 1: "Sender (untrusted metadata): ```json {...}``` text"
25
+ * - Feishu wrapper format 2: "Conversation info (untrusted metadata): ```json {...}``` text"
26
+ * - Clean user message text
27
+ *
28
+ * Also detects empathy observer output (to prevent recursive spawn) and
29
+ * agent-to-agent messages (to skip empathy evaluation).
30
+ *
31
+ * Pure logic — no I/O, no side effects.
32
+ */
33
+ export declare function extractUserMessageFromPrompt(prompt: string, sessionId: string | undefined): ExtractedUserMessage;
34
+ /**
35
+ * Build the minimal Agent Identity section for prependSystemContext.
36
+ *
37
+ * EvolutionWorker-era INTERNAL SYSTEM LAYOUT removed per PRI-294.
38
+ * The EVOLUTION_WORKER PathResolver key and system layout reference are
39
+ * not MVP-Core; agents discover what they need via tool calls.
40
+ *
41
+ * Pure logic — returns a constant string.
42
+ */
43
+ export declare function buildAgentIdentity(): string;
44
+ /**
45
+ * Build the empathy output restriction constraint text.
46
+ *
47
+ * Pure logic — returns a constant string.
48
+ */
49
+ export declare function buildEmpathySilenceConstraint(): string;
50
+ /**
51
+ * Wrap heartbeat checklist content in XML tags.
52
+ *
53
+ * Pure logic — no I/O, no side effects.
54
+ */
55
+ export declare function assembleHeartbeatChecklist(content: string): string;
56
+ /**
57
+ * Format core principles into prompt-ready text.
58
+ *
59
+ * Pure logic — uses escapeXml for safe XML embedding.
60
+ *
61
+ * @param principles Active principles from evolution reducer
62
+ * @returns Formatted lines (empty string if no principles)
63
+ */
64
+ export declare function formatCorePrinciples(principles: CorePrincipleEntry[]): string;
65
+ /**
66
+ * Format evolution principles (active + probation) into prompt-ready text.
67
+ *
68
+ * Pure logic — uses escapeXml for safe XML embedding.
69
+ *
70
+ * @param active Active principles (high priority)
71
+ * @param probation Probation principles (contextual, caution)
72
+ * @returns Formatted lines (empty string if no principles)
73
+ */
74
+ export declare function formatEvolutionPrinciples(active: EvolutionPrincipleEntry[], probation: EvolutionPrincipleEntry[]): string;
75
+ /**
76
+ * Assemble appendSystemContext from ordered parts.
77
+ *
78
+ * Content order (most important last):
79
+ * behavioral_constraints → project_context → working_memory →
80
+ * thinking_os → evolution_principles → core_principles
81
+ *
82
+ * Pure logic — string assembly only, no I/O.
83
+ *
84
+ * @param parts Ordered content parts (empty/undefined parts are skipped)
85
+ * @returns Assembled appendSystemContext (empty string if no parts)
86
+ */
87
+ export declare function assembleAppendSystemContext(parts: AppendSystemContextParts): string;