@principles/host-runtime 0.6.0 → 0.7.1

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,8 +8,10 @@ type Degradation = {
8
8
  reason: string;
9
9
  nextAction: string;
10
10
  };
11
- /** learned 词进入高精度 deterministic path 的权重阈值(仅 seed/owner_promoted; llm_learned 恒 ambiguous) */
11
+ /** seed/owner_promoted 词进入高精度 deterministic path 的权重阈值 */
12
12
  export declare const HIGH_PRECISION_LEARNED_WEIGHT = 0.7;
13
+ /** PRI-788 G3: llm 学习词 earned precision 的 TP 门槛(且要求零 FP) */
14
+ export declare const EARNED_PRECISION_MIN_TP = 3;
13
15
  /** 高精度纠正短语 overlay(已验证的确定性 STRONG 路径,不属于 learner seed 集) */
14
16
  export declare const HIGH_PRECISION_CORRECTION_OVERLAY: readonly (readonly [string, number])[];
15
17
  /** empathy seed overlay(检测行为不变) */
@@ -63,13 +63,21 @@ function rowField(row, key) {
63
63
  }
64
64
  // ─── Shared correction keyword store (host-neutral extraction) ──────────────
65
65
  const KEYWORD_STORE_FILE = 'correction_keywords.json';
66
- /** learned 词进入高精度 deterministic path 的权重阈值(仅 seed/owner_promoted; llm_learned 恒 ambiguous) */
66
+ /** seed/owner_promoted 词进入高精度 deterministic path 的权重阈值 */
67
67
  export const HIGH_PRECISION_LEARNED_WEIGHT = 0.7;
68
+ /** PRI-788 G3: llm 学习词 earned precision 的 TP 门槛(且要求零 FP) */
69
+ export const EARNED_PRECISION_MIN_TP = 3;
68
70
  /** 高精度纠正短语 overlay(已验证的确定性 STRONG 路径,不属于 learner seed 集) */
69
71
  export const HIGH_PRECISION_CORRECTION_OVERLAY = [
70
72
  ['这是错的', 0.9],
71
73
  ['不要自作主张', 0.9],
72
74
  ['不应该这么做', 0.9],
75
+ // PRI-788 G3(Owner 批准清单):中文高精度纠正句式
76
+ ['我说的是', 0.9],
77
+ ['不是让你', 0.85],
78
+ ['谁让你', 0.85],
79
+ ['又搞错', 0.8],
80
+ ['都说了', 0.8],
73
81
  ];
74
82
  /** empathy seed overlay(检测行为不变) */
75
83
  export const EMPATHY_SEED_OVERLAY = [
@@ -83,6 +91,14 @@ function isValidLearnedKeyword(v) {
83
91
  return false;
84
92
  if (typeof k.weight !== 'number' || !Number.isFinite(k.weight))
85
93
  return false;
94
+ // earned precision 的计数是安全门(tp >= 3 才升 high),必须先约束数值形状:
95
+ // `correction_keywords.json` 里被篡改/损坏的 `truePositiveCount: 3.1` 若放行,
96
+ // 会让只有 3 条样本(甚至根本没有样本)的词项挤进 high 确定性路径。
97
+ // 缺失按可选字段语义放行,存在则必须是【非负安全整数】。
98
+ const isCount = (value) => value === undefined ||
99
+ (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0);
100
+ if (!isCount(k.truePositiveCount) || !isCount(k.falsePositiveCount))
101
+ return false;
86
102
  return k.source === 'seed' || k.source === 'llm' || k.source === 'user';
87
103
  }
88
104
  function mapLearnedSource(source) {
@@ -92,9 +108,18 @@ function mapLearnedSource(source) {
92
108
  return 'owner_promoted';
93
109
  return 'seed';
94
110
  }
95
- function precisionFor(source, weight) {
96
- if (source === 'llm')
97
- return 'ambiguous';
111
+ /**
112
+ * PRI-788 G3 earned precision:llm 学习词不再恒为 ambiguous——确认证据达标
113
+ * (TP ≥ 3 且零 FP)时升 high,出现任一 FP 即降回 ambiguous。安全姿态不变:
114
+ * LLM 单次建议无法自升 STRONG,必须积累 LLM 确认的正例。owner_promoted 仍按
115
+ * 权重判定(Owner 显式决策不受证据门槛约束)。
116
+ */
117
+ function precisionFor(source, weight, stats) {
118
+ if (source === 'llm') {
119
+ const tp = stats?.truePositiveCount ?? 0;
120
+ const fp = stats?.falsePositiveCount ?? 0;
121
+ return tp >= EARNED_PRECISION_MIN_TP && fp === 0 ? 'high' : 'ambiguous';
122
+ }
98
123
  return weight >= HIGH_PRECISION_LEARNED_WEIGHT ? 'high' : 'ambiguous';
99
124
  }
100
125
  function projectLearnedStore(raw) {
@@ -116,7 +141,7 @@ function projectLearnedStore(raw) {
116
141
  term,
117
142
  category: 'correction',
118
143
  weight,
119
- precision: precisionFor(kw.source, weight),
144
+ precision: precisionFor(kw.source, weight, kw),
120
145
  source: mapLearnedSource(kw.source),
121
146
  };
122
147
  if (kw.source === 'llm')
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type HostEvent, type HostEventResult } from '@principles/core/host';
1
+ import { type HostEvent, type HostEventResult, type HostEventEmitter } from '@principles/core/host';
2
2
  import { buildActivePrinciplePromptContext } from './active-principle-prompt.js';
3
3
  import { type RuleContextProvider, type RuleInputEnrichmentProvider } from './production-rulehost-gate.js';
4
4
  import type { RuleImplementationRuntime } from './rule-implementation-runtime.js';
@@ -67,4 +67,13 @@ export declare function createProductionHostRuntime(options?: Partial<Pick<HostR
67
67
  hostKind?: GovernanceHostKind;
68
68
  /** PRI-634-F: host-declared tool semantics supplied by the constructing host adapter. */
69
69
  toolSemantics?: ToolSemanticRegistry;
70
+ /**
71
+ * PRI-750: optional event emission port. When present, the shared-path
72
+ * handlers record injection/tool events carrying the host's natural
73
+ * turn/tool ids (turnId/toolCallId) in the same events_*.jsonl format as
74
+ * the OpenClaw path (whose events additionally bind to
75
+ * assistant_turns.run_id). Only the Codex host adapter wires this; the
76
+ * OpenClaw plugin path owns its own emission.
77
+ */
78
+ events?: HostEventEmitter;
70
79
  }): HostRuntime;
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isHostEvent, isHostEventResult, } from '@principles/core/host';
2
+ import { RUNTIME_V2_PRINCIPLE_BUDGET } from '@principles/core/runtime-v2';
2
3
  import { buildActivePrinciplePromptContext } from './active-principle-prompt.js';
3
4
  import { createProductionRuleHostGate } from './production-rulehost-gate.js';
4
5
  import { createProductionPainEvidenceHandler } from './production-pain-evidence.js';
@@ -171,6 +172,7 @@ export function createProductionHostRuntime(options = {}) {
171
172
  ...(options.painEnrichmentProvider ? { painEnrichmentProvider: options.painEnrichmentProvider } : {}),
172
173
  ...(options.painDatabaseFactory ? { painDatabaseFactory: options.painDatabaseFactory } : {}),
173
174
  ...(options.hostKind ? { hostKind: options.hostKind } : {}),
175
+ ...(options.events ? { events: options.events } : {}),
174
176
  }),
175
177
  beforeToolCall: options.beforeToolCall ?? productionGate,
176
178
  async beforePromptBuild(event) {
@@ -178,12 +180,47 @@ export function createProductionHostRuntime(options = {}) {
178
180
  workspaceDir: event.context.workspaceDir,
179
181
  excludePrincipleIds: options.promptExcludePrincipleIds?.(event),
180
182
  });
181
- if (options.beforePromptBuild)
182
- return options.beforePromptBuild(event, prompt);
183
+ // PRI-750: record the injection event on the shared path with the host
184
+ // turn id (Codex turn_id → runId) so receipt events carry a turn-level
185
+ // anchor in the same events_*.jsonl format as the OpenClaw path. The
186
+ // OpenClaw plugin additionally persists these to assistant_turns.run_id;
187
+ // the Codex DB-side anchor is a follow-up. Optional port — absent means
188
+ // no-op (the OpenClaw plugin path emits this event itself). Emission
189
+ // failure must not block the prompt result — it degrades to an
190
+ // observable warning (rc-9).
191
+ const emissionWarnings = [];
192
+ try {
193
+ options.events?.recordRuntimeV2ActivationsInjected({
194
+ sessionId: event.context.sessionId,
195
+ workspaceDir: event.context.workspaceDir,
196
+ principleIds: prompt.principleIds,
197
+ activationIds: prompt.activationIds,
198
+ artifactIds: prompt.artifactIds,
199
+ injectedCount: prompt.principleIds.length,
200
+ skippedWarnings: prompt.warnings,
201
+ injectedCharCount: prompt.additionalContext.length,
202
+ budget: RUNTIME_V2_PRINCIPLE_BUDGET,
203
+ ...(prompt.truncated !== undefined ? { v2Truncated: prompt.truncated } : {}),
204
+ ...(event.context.turnId !== undefined ? { runId: event.context.turnId } : {}),
205
+ });
206
+ }
207
+ catch (err) {
208
+ emissionWarnings.push(`receipt_event_write_failed:${err instanceof Error ? err.message.slice(0, 200) : String(err).slice(0, 200)}`);
209
+ }
210
+ // PRI-750 review: a custom beforePromptBuild must not swallow the
211
+ // receipt-emission warnings captured above (rc-9) — merge them into the
212
+ // handler result instead of replacing the result wholesale.
213
+ if (options.beforePromptBuild) {
214
+ const custom = await options.beforePromptBuild(event, prompt);
215
+ if (emissionWarnings.length === 0)
216
+ return custom;
217
+ return { ...custom, warnings: [...(custom.warnings ?? []), ...emissionWarnings] };
218
+ }
183
219
  return {
184
220
  decision: prompt.additionalContext.length > 0 ? 'modify' : 'allow',
185
221
  source: event.source,
186
222
  ...(prompt.additionalContext.length > 0 ? { additionalContext: prompt.additionalContext } : {}),
223
+ ...(emissionWarnings.length > 0 ? { warnings: emissionWarnings } : {}),
187
224
  };
188
225
  },
189
226
  });
@@ -1,6 +1,6 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import type { GovernanceHostKind } from '@principles/core/runtime-v2';
3
- import type { HostEvent, HostEventResult } from '@principles/core/host';
3
+ import type { HostEvent, HostEventEmitter, HostEventResult } from '@principles/core/host';
4
4
  /** Shared with the governance admission path so both gate tool failures identically. */
5
5
  export declare const PRODUCTION_WRITE_TOOLS: ReadonlySet<string>;
6
6
  export interface PainEvidenceEntry {
@@ -85,6 +85,7 @@ export declare function createProductionPainEvidenceHandler(options?: {
85
85
  painEnrichmentProvider?: PainEnrichmentProvider;
86
86
  painDatabaseFactory?: PainDatabaseFactory;
87
87
  hostKind?: GovernanceHostKind;
88
+ events?: HostEventEmitter;
88
89
  }): (event: HostEvent) => Promise<HostEventResult>;
89
90
  export declare function resetProductionPainCooldownForTest(): void;
90
91
  export declare function productionPainCooldownEntryCountForTest(): number;
@@ -234,9 +234,31 @@ function hasCanonicalSchema(db) {
234
234
  */
235
235
  export function createProductionPainEvidenceHandler(options = {}) {
236
236
  return async (event) => {
237
+ // PRI-750: shared-path tool event with the host's natural turn/tool ids
238
+ // (Codex turn_id → runId, tool_use_id → toolCallId) so receipt events
239
+ // carry a turn-level anchor in the same events_*.jsonl format as the
240
+ // OpenClaw path. Emitted BEFORE the trajectory-db availability gate so
241
+ // the receipt chain stays observable even when the trajectory store is
242
+ // unavailable. Emission failure must not block the tool result — it
243
+ // degrades to an observable warning (rc-9). Optional port: absent means
244
+ // no-op (the OpenClaw plugin path emits its own tool events).
245
+ let emissionWarning;
246
+ try {
247
+ const earlyOutcome = normalizeOutcome(event);
248
+ options.events?.recordToolCall(event.context.sessionId, {
249
+ toolName: event.context.toolName ?? '',
250
+ ...(earlyOutcome.error ? { error: earlyOutcome.error } : {}),
251
+ ...(earlyOutcome.exitCode !== 0 ? { exitCode: earlyOutcome.exitCode } : {}),
252
+ ...(event.context.turnId !== undefined ? { runId: event.context.turnId } : {}),
253
+ ...(event.context.toolCallId !== undefined ? { toolCallId: event.context.toolCallId } : {}),
254
+ });
255
+ }
256
+ catch (err) {
257
+ emissionWarning = `receipt_event_write_failed:${err instanceof Error ? err.message.slice(0, 200) : String(err).slice(0, 200)}`;
258
+ }
237
259
  const dbPath = path.join(event.context.workspaceDir, '.state', 'trajectory.db');
238
260
  if (!fs.existsSync(dbPath)) {
239
- return { decision: 'observe', source: event.source, warnings: ['trajectory_db_not_found'], metadata: { outcome: 'unavailable', admitted: false, duplicate: false, nextAction: 'initialize the selected PD workspace before retrying the hook' } };
261
+ return { decision: 'observe', source: event.source, warnings: [...(emissionWarning ? [emissionWarning] : []), 'trajectory_db_not_found'], metadata: { outcome: 'unavailable', admitted: false, duplicate: false, nextAction: 'initialize the selected PD workspace before retrying the hook' } };
240
262
  }
241
263
  let db;
242
264
  try {
@@ -245,7 +267,7 @@ export function createProductionPainEvidenceHandler(options = {}) {
245
267
  if (!hasCanonicalSchema(db)) {
246
268
  db.close();
247
269
  db = undefined;
248
- return { decision: 'observe', source: event.source, warnings: ['trajectory_schema_invalid'], metadata: { outcome: 'unavailable', admitted: false, duplicate: false, nextAction: 'run the supported PD workspace migration' } };
270
+ return { decision: 'observe', source: event.source, warnings: [...(emissionWarning ? [emissionWarning] : []), 'trajectory_schema_invalid'], metadata: { outcome: 'unavailable', admitted: false, duplicate: false, nextAction: 'run the supported PD workspace migration' } };
249
271
  }
250
272
  }
251
273
  catch (error) {
@@ -253,7 +275,7 @@ export function createProductionPainEvidenceHandler(options = {}) {
253
275
  db?.close();
254
276
  }
255
277
  catch { /* best-effort cleanup of an unusable handle */ }
256
- return { decision: 'observe', source: event.source, warnings: [`trajectory_database_unavailable:${error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200)}`], metadata: { outcome: 'unavailable', admitted: false, duplicate: false, nextAction: 'inspect or repair the selected PD trajectory database' } };
278
+ return { decision: 'observe', source: event.source, warnings: [...(emissionWarning ? [emissionWarning] : []), `trajectory_database_unavailable:${error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200)}`], metadata: { outcome: 'unavailable', admitted: false, duplicate: false, nextAction: 'inspect or repair the selected PD trajectory database' } };
257
279
  }
258
280
  const warnings = [];
259
281
  let enrichment;
@@ -269,7 +291,7 @@ export function createProductionPainEvidenceHandler(options = {}) {
269
291
  db.close();
270
292
  }
271
293
  catch { /* no business write occurred */ }
272
- return { decision: 'observe', source: event.source, warnings: warnings.length > 0 ? warnings : ['pain_enrichment_invalid'], metadata: { outcome: 'unavailable', admitted: false, duplicate: false, nextAction: 'inspect host pain enrichment input' } };
294
+ return { decision: 'observe', source: event.source, warnings: [...(emissionWarning ? [emissionWarning] : []), ...(warnings.length > 0 ? warnings : ['pain_enrichment_invalid'])], metadata: { outcome: 'unavailable', admitted: false, duplicate: false, nextAction: 'inspect host pain enrichment input' } };
273
295
  }
274
296
  const outcome = normalizeOutcome(event);
275
297
  const sanitizedParams = sanitizeToolParams(outcome.params, event.context.workspaceDir);
@@ -342,7 +364,7 @@ export function createProductionPainEvidenceHandler(options = {}) {
342
364
  }
343
365
  }
344
366
  catch (error) {
345
- return { decision: 'observe', source: event.source, warnings: [`trajectory_write_failed:${error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200)}`], metadata: { outcome: outcome.failure ? 'failure' : 'success', admitted: false, duplicate: false, nextAction: 'inspect the workspace trajectory database and retry' } };
367
+ return { decision: 'observe', source: event.source, warnings: [...(emissionWarning ? [emissionWarning] : []), `trajectory_write_failed:${error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200)}`], metadata: { outcome: outcome.failure ? 'failure' : 'success', admitted: false, duplicate: false, nextAction: 'inspect the workspace trajectory database and retry' } };
346
368
  }
347
369
  finally {
348
370
  try {
@@ -351,12 +373,17 @@ export function createProductionPainEvidenceHandler(options = {}) {
351
373
  catch { /* write result already determined; cleanup is best-effort */ }
352
374
  }
353
375
  const effectiveAdmitted = admitted || duplicateAdmitted;
354
- return { decision: 'observe', source: event.source, metadata: {
376
+ return {
377
+ decision: 'observe',
378
+ source: event.source,
379
+ ...(emissionWarning ? { warnings: [emissionWarning] } : {}),
380
+ metadata: {
355
381
  eventId, painId: effectiveAdmitted ? painId : null, outcome: outcome.failure ? 'failure' : 'success', admitted: effectiveAdmitted, duplicate,
356
382
  sourceKind, failureSource: sourceObservation.failureSource ?? null, triggerOutcome: trigger.outcome,
357
383
  triggerReason: trigger.reason, painScore, isRisky, relativePath, agentId: enrichment.agentId ?? null,
358
384
  evidence: enrichment.evidence ?? [],
359
- } };
385
+ }
386
+ };
360
387
  };
361
388
  }
362
389
  export function resetProductionPainCooldownForTest() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/host-runtime",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Shared host-neutral orchestration for Principles Disciple MVP-Core hook paths.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",