@principles/host-runtime 0.9.2 → 0.10.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.
- package/dist/behavior-example-pack-assembler.d.ts +41 -0
- package/dist/behavior-example-pack-assembler.js +243 -0
- package/dist/behavior-history.d.ts +6 -0
- package/dist/behavior-history.js +76 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/internalization-consumer-cycle.d.ts +10 -1
- package/dist/internalization-consumer-cycle.js +85 -4
- package/dist/production-rulehost-gate.js +40 -3
- package/dist/workspace-behavior-evidence.d.ts +3 -0
- package/dist/workspace-behavior-evidence.js +74 -0
- package/package.json +12 -4
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { BehaviorExamplePack } from '@principles/core/runtime-v2';
|
|
2
|
+
import type { RuleHostEvidenceRow, RuleHostContextRow } from '@principles/core/trajectory-types';
|
|
3
|
+
import type { ToolSemanticRegistry } from '@principles/core/runtime-v2';
|
|
4
|
+
export interface BehaviorExampleDataSource {
|
|
5
|
+
getPainEventByCanonicalId(id: string): {
|
|
6
|
+
id: number;
|
|
7
|
+
sessionId: string;
|
|
8
|
+
} | null;
|
|
9
|
+
getRuleHostEvidenceRow(id: number): RuleHostEvidenceRow | null;
|
|
10
|
+
getRuleHostContextRowsBefore(sessionId: string, beforeId: number, limit: number): {
|
|
11
|
+
rows: readonly RuleHostContextRow[];
|
|
12
|
+
truncated: boolean;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface BehaviorExamplePackAssemblerOptions {
|
|
16
|
+
readonly source: BehaviorExampleDataSource;
|
|
17
|
+
readonly toolSemantics: ToolSemanticRegistry;
|
|
18
|
+
}
|
|
19
|
+
export interface BehaviorExamplePackAssemblerInput {
|
|
20
|
+
/** Canonical pain ID (e.g. pain_<ts>_<hash>) — entry point into pain lineage. */
|
|
21
|
+
readonly sourcePainId: string;
|
|
22
|
+
/** Owner's natural-language desired outcome — must be non-empty. */
|
|
23
|
+
readonly ownerDesiredOutcome: string;
|
|
24
|
+
/** Owner-selected call that demonstrates behaviour to block. */
|
|
25
|
+
readonly sourceNegativeToolCallId: number;
|
|
26
|
+
/** Owner-selected calls that demonstrate behaviour to allow (1..3). */
|
|
27
|
+
readonly positiveToolCallIds: readonly number[];
|
|
28
|
+
/**
|
|
29
|
+
* Project directory, used to detect absolute paths that should be redacted
|
|
30
|
+
* to relative/basename form in the assembled pack.
|
|
31
|
+
*/
|
|
32
|
+
readonly projectDir: string;
|
|
33
|
+
}
|
|
34
|
+
export declare class BehaviorExamplePackAssembler {
|
|
35
|
+
private readonly db;
|
|
36
|
+
private readonly toolSemantics;
|
|
37
|
+
constructor(opts: BehaviorExamplePackAssemblerOptions);
|
|
38
|
+
/** Assemble only from explicit Owner labels; execution outcome is not a label. */
|
|
39
|
+
assemble(input: BehaviorExamplePackAssemblerInput): BehaviorExamplePack;
|
|
40
|
+
private buildSelectedCase;
|
|
41
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Owner-labelled BehaviorExamplePack assembly (PRI-484 / PRI-992).
|
|
3
|
+
*
|
|
4
|
+
* Reads pain lineage + trajectory rows from TrajectoryDatabase and assembles
|
|
5
|
+
* a validated BehaviorExamplePack for the Artificer. Fail loud on any gap
|
|
6
|
+
* (missing pain, empty trajectory, invalid pack) — no silent fallback.
|
|
7
|
+
*
|
|
8
|
+
* Err-prevention:
|
|
9
|
+
* - ERR-001: every DB row field validated as `unknown` (no `as` bypass on
|
|
10
|
+
* parsed params_json or row casts).
|
|
11
|
+
* - ERR-069: Artificer shared schema — invalid pack must fail loud.
|
|
12
|
+
* - ERR-026: reuses production TrajectoryDatabase (no hand-written DDL).
|
|
13
|
+
* - rc-2: structural type guards, no `as T` on parsed input.
|
|
14
|
+
* - rc-5: Object.hasOwn over `in`.
|
|
15
|
+
* - rc-9: every failure path throws a structured Error with a clear reason.
|
|
16
|
+
* - PRI-980 review P2-1: trajectory STORE I/O failures throw the typed
|
|
17
|
+
* `evidence_source_error` (readTrajectoryStore) so the readiness contract
|
|
18
|
+
* preserves them as storage faults, not Owner evidence faults (§6.2).
|
|
19
|
+
*
|
|
20
|
+
* Spec: docs/superpowers/specs/2026-06-27-rulecode-context-vision-design.md §7.2
|
|
21
|
+
*
|
|
22
|
+
* Boundary: this module is I/O. Pure logic (type + validator) lives in
|
|
23
|
+
* `@principles/core/runtime-v2` → `behavior-example-pack.ts`.
|
|
24
|
+
*/
|
|
25
|
+
import { buildRuleHostAction } from '@principles/core/runtime-v2/internalization/rule-host-input-builder';
|
|
26
|
+
import { computeBehaviorFacts } from '@principles/core/runtime-v2/internalization/rule-context-v2';
|
|
27
|
+
import { validateBehaviorExamplePack } from '@principles/core/runtime-v2/internalization/behavior-example-pack';
|
|
28
|
+
// Factory lives in evidence-readiness.ts (the classification authority); it is
|
|
29
|
+
// consumed through its registered LEAF subpath. A VALUE import of the
|
|
30
|
+
// runtime-v2 barrel on this satellite path re-pulls the whole LLM graph and
|
|
31
|
+
// fails the satellite-purity guard (OPT-002, scripts/build/check-satellite-purity.mjs) —
|
|
32
|
+
// the leaf is on core's exports allowlist for exactly this consumer.
|
|
33
|
+
import { createOwnerEvidenceScopeError } from '@principles/core/runtime-v2/internalization/evidence-readiness';
|
|
34
|
+
import { assembleBehaviorHistory } from './behavior-history.js';
|
|
35
|
+
// ── internal constants ─────────────────────────────────────────────────────
|
|
36
|
+
const MAX_POSITIVES = 3;
|
|
37
|
+
const MAX_EVIDENCE_REFS = 5;
|
|
38
|
+
const HISTORY_LIMIT = 100;
|
|
39
|
+
const PROTO_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
40
|
+
/**
|
|
41
|
+
* PRI-980 review P2-1 (SPEC-PD-IAER-001 §6.2 case 3): the trajectory STORE
|
|
42
|
+
* throwing is an I/O/storage failure of the evidence source itself — it must
|
|
43
|
+
* surface as the typed `evidence_source_error` kind so the readiness contract
|
|
44
|
+
* preserves it as a storage fault instead of misreporting "Owner labelled bad
|
|
45
|
+
* evidence". Structural, never message sniffing: the store RETURNING no row
|
|
46
|
+
* is a data gap (the typed scope kinds below); the store THROWING is the I/O
|
|
47
|
+
* failure signal. Assembler-owned data-shape validation errors stay plain
|
|
48
|
+
* Errors (→ `invalid`), unchanged.
|
|
49
|
+
*/
|
|
50
|
+
function readTrajectoryStore(what, op) {
|
|
51
|
+
try {
|
|
52
|
+
return op();
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
56
|
+
throw createOwnerEvidenceScopeError('evidence_source_error', `[BehaviorExamplePackAssembler] trajectory store ${what} failed (evidence source error, SPEC-PD-IAER-001 §6.2): ${message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// ── type guards (rc-2: no `as` bypass) ─────────────────────────────────────
|
|
60
|
+
function isPlainObject(value) {
|
|
61
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
62
|
+
}
|
|
63
|
+
function hasNoProtoKeys(value) {
|
|
64
|
+
for (const key of Object.keys(value)) {
|
|
65
|
+
if (PROTO_KEYS.has(key))
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
// ── BehaviorExamplePackAssembler ───────────────────────────────────────────
|
|
71
|
+
/**
|
|
72
|
+
* Host-neutral assembler: pain lineage + trajectory → validated BehaviorExamplePack.
|
|
73
|
+
*
|
|
74
|
+
* The caller owns source lifecycle and supplies the real host vocabulary.
|
|
75
|
+
*/
|
|
76
|
+
function validateOwnerSelection(input) {
|
|
77
|
+
if (!Number.isSafeInteger(input.sourceNegativeToolCallId) || input.sourceNegativeToolCallId <= 0) {
|
|
78
|
+
throw new Error('[BehaviorExamplePackAssembler] sourceNegativeToolCallId must be a positive integer');
|
|
79
|
+
}
|
|
80
|
+
if (!Array.isArray(input.positiveToolCallIds) || input.positiveToolCallIds.length === 0 || input.positiveToolCallIds.length > MAX_POSITIVES) {
|
|
81
|
+
throw new Error('[BehaviorExamplePackAssembler] positiveToolCallIds must contain 1 to at most 3 IDs');
|
|
82
|
+
}
|
|
83
|
+
const allIds = [input.sourceNegativeToolCallId, ...input.positiveToolCallIds];
|
|
84
|
+
if (allIds.some((id) => !Number.isSafeInteger(id) || id <= 0) || new Set(allIds).size !== allIds.length) {
|
|
85
|
+
throw new Error('[BehaviorExamplePackAssembler] selected tool call IDs must be unique positive integers');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function requireSelectedRow(row, id) {
|
|
89
|
+
if (!row) {
|
|
90
|
+
// PRI-983: typed scope classification → stable `owner_behavior_examples_stale`
|
|
91
|
+
// at the readiness contract (the referenced trajectory row does not exist).
|
|
92
|
+
throw createOwnerEvidenceScopeError('tool_call_not_found', `[BehaviorExamplePackAssembler] selected tool call id=${id} not found`);
|
|
93
|
+
}
|
|
94
|
+
return row;
|
|
95
|
+
}
|
|
96
|
+
// ── internal: case builder ─────────────────────────────────────────────────
|
|
97
|
+
/**
|
|
98
|
+
* Build a GoldenTraceCaseInput from a trajectory tool_call row.
|
|
99
|
+
*
|
|
100
|
+
* ERR-001: parses params_json as unknown and structurally validates (must be
|
|
101
|
+
* a plain object without proto-pollution keys). Throws on malformed input
|
|
102
|
+
* (rc-9: no silent fallback).
|
|
103
|
+
*/
|
|
104
|
+
function buildCaseFromRow(row, kind, expectedDecision) {
|
|
105
|
+
if (typeof row.toolName !== 'string' || row.toolName.length === 0) {
|
|
106
|
+
throw new Error(`[BehaviorExamplePackAssembler] row id=${row.id}: toolName must be a non-empty string (rc-9 fail loud)`);
|
|
107
|
+
}
|
|
108
|
+
if (typeof row.paramsJson !== 'string') {
|
|
109
|
+
throw new Error(`[BehaviorExamplePackAssembler] row id=${row.id}: paramsJson must be a string (rc-9 fail loud)`);
|
|
110
|
+
}
|
|
111
|
+
let parsedParams;
|
|
112
|
+
try {
|
|
113
|
+
parsedParams = JSON.parse(row.paramsJson);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
throw new Error(`[BehaviorExamplePackAssembler] row id=${row.id}: paramsJson is not valid JSON (rc-9 fail loud)`);
|
|
117
|
+
}
|
|
118
|
+
if (!isPlainObject(parsedParams)) {
|
|
119
|
+
throw new Error(`[BehaviorExamplePackAssembler] row id=${row.id}: paramsJson must parse to a non-array object (rc-9 fail loud)`);
|
|
120
|
+
}
|
|
121
|
+
if (!hasNoProtoKeys(parsedParams)) {
|
|
122
|
+
throw new Error(`[BehaviorExamplePackAssembler] row id=${row.id}: paramsJson must not carry prototype-pollution keys (ERR-076, rc-9 fail loud)`);
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
caseId: `case-${kind}-${row.id}`,
|
|
126
|
+
kind,
|
|
127
|
+
toolName: row.toolName,
|
|
128
|
+
params: parsedParams,
|
|
129
|
+
expectedDecision,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Redact sensitive fields from tool call params before they enter the pack.
|
|
134
|
+
*
|
|
135
|
+
* First-version policy:
|
|
136
|
+
* - Absolute paths outside the project tree → replaced with `<redacted:absolute-path>`
|
|
137
|
+
* - String values that look like secrets (long base64 / AKIA-style keys / etc.)
|
|
138
|
+
* → replaced with `<redacted:secret>`
|
|
139
|
+
*
|
|
140
|
+
* Records each redaction in `notes` so the Artificer and the owner can see
|
|
141
|
+
* what was stripped (spec §7.2: redactionNotes is part of the pack contract).
|
|
142
|
+
*/
|
|
143
|
+
function looksLikeAbsolutePath(value) {
|
|
144
|
+
// POSIX absolute path or Windows drive path
|
|
145
|
+
return value.startsWith('/') || /^[A-Za-z]:[\\/]/.test(value);
|
|
146
|
+
}
|
|
147
|
+
const SECRET_PATTERNS = [
|
|
148
|
+
/AKIA[0-9A-Z]{16}/, // AWS access key ID
|
|
149
|
+
/[A-Za-z0-9+/]{40,}/, // long base64-ish token (≥40 chars, no spaces)
|
|
150
|
+
];
|
|
151
|
+
function looksLikeSecret(value) {
|
|
152
|
+
if (value.length < 20)
|
|
153
|
+
return false; // too short to be a real secret
|
|
154
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
155
|
+
if (pattern.test(value))
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
function redactParams(params, projectDir) {
|
|
161
|
+
const notes = [];
|
|
162
|
+
const redacted = {};
|
|
163
|
+
for (const key of Object.keys(params)) {
|
|
164
|
+
if (PROTO_KEYS.has(key))
|
|
165
|
+
continue; // defense in depth (ERR-076)
|
|
166
|
+
const value = params[key];
|
|
167
|
+
if (typeof value !== 'string') {
|
|
168
|
+
redacted[key] = value;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
// Path-like values
|
|
172
|
+
if (looksLikeAbsolutePath(value) && !value.startsWith(projectDir)) {
|
|
173
|
+
redacted[key] = '<redacted:absolute-path>';
|
|
174
|
+
notes.push(`redacted absolute path in field "${key}" (outside project tree)`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
// Secret-like values (AWS keys, long base64 tokens, etc.)
|
|
178
|
+
if (looksLikeSecret(value)) {
|
|
179
|
+
redacted[key] = '<redacted:secret>';
|
|
180
|
+
notes.push(`redacted possible secret in field "${key}"`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
redacted[key] = value;
|
|
184
|
+
}
|
|
185
|
+
return { redacted, notes };
|
|
186
|
+
}
|
|
187
|
+
export class BehaviorExamplePackAssembler {
|
|
188
|
+
db;
|
|
189
|
+
toolSemantics;
|
|
190
|
+
constructor(opts) {
|
|
191
|
+
this.db = opts.source;
|
|
192
|
+
this.toolSemantics = opts.toolSemantics;
|
|
193
|
+
}
|
|
194
|
+
/** Assemble only from explicit Owner labels; execution outcome is not a label. */
|
|
195
|
+
assemble(input) {
|
|
196
|
+
const pain = readTrajectoryStore(`pain lineage read for canonical_pain_id="${input.sourcePainId}"`, () => this.db.getPainEventByCanonicalId(input.sourcePainId));
|
|
197
|
+
if (!pain) {
|
|
198
|
+
// PRI-983: typed scope classification (structural marker — the readiness
|
|
199
|
+
// contract maps this to the stable `owner_behavior_examples_stale`
|
|
200
|
+
// code; FR-03: an unresolvable pain reference must never be silently
|
|
201
|
+
// reused). Message text unchanged (historical audit signature).
|
|
202
|
+
throw createOwnerEvidenceScopeError('pain_not_found', `[BehaviorExamplePackAssembler] pain event not found for canonical_pain_id="${input.sourcePainId}" (ERR-069 fail loud)`);
|
|
203
|
+
}
|
|
204
|
+
validateOwnerSelection(input);
|
|
205
|
+
const negativeRow = requireSelectedRow(readTrajectoryStore(`evidence row read for id=${input.sourceNegativeToolCallId}`, () => this.db.getRuleHostEvidenceRow(input.sourceNegativeToolCallId)), input.sourceNegativeToolCallId);
|
|
206
|
+
const positiveRows = input.positiveToolCallIds.map((id) => requireSelectedRow(readTrajectoryStore(`evidence row read for id=${id}`, () => this.db.getRuleHostEvidenceRow(id)), id));
|
|
207
|
+
for (const row of [negativeRow, ...positiveRows]) {
|
|
208
|
+
if (row.sessionId !== pain.sessionId) {
|
|
209
|
+
// PRI-983: cross pain/session selection → typed scope error → stable
|
|
210
|
+
// `owner_behavior_examples_scope_mismatch` at the readiness contract.
|
|
211
|
+
throw createOwnerEvidenceScopeError('session_mismatch', `[BehaviorExamplePackAssembler] selected tool call ${row.id} session mismatch; all examples must belong to pain session ${pain.sessionId}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const redactionNotes = [];
|
|
215
|
+
const buildCtx = { projectDir: input.projectDir, redactionNotes };
|
|
216
|
+
const sourceNegativeCase = this.buildSelectedCase(negativeRow, 'negative', buildCtx);
|
|
217
|
+
const positiveCounterexamples = positiveRows.map((row) => this.buildSelectedCase(row, 'positive', buildCtx));
|
|
218
|
+
const pack = {
|
|
219
|
+
sourceNegativeCase,
|
|
220
|
+
ownerDesiredOutcome: input.ownerDesiredOutcome,
|
|
221
|
+
positiveCounterexamples,
|
|
222
|
+
evidenceRefs: [`pain:${pain.id}`, `tool_call:${negativeRow.id}`, ...positiveRows.map((row) => `tool_call:${row.id}`)].slice(0, MAX_EVIDENCE_REFS),
|
|
223
|
+
redactionNotes,
|
|
224
|
+
};
|
|
225
|
+
// ── 9. Validate — fail loud (ERR-069) ──
|
|
226
|
+
const validation = validateBehaviorExamplePack(pack);
|
|
227
|
+
if (!validation.valid) {
|
|
228
|
+
throw new Error(`[BehaviorExamplePackAssembler] assembled pack failed validateBehaviorExamplePack: ${validation.errors.join('; ')} (ERR-069 fail loud)`);
|
|
229
|
+
}
|
|
230
|
+
return pack;
|
|
231
|
+
}
|
|
232
|
+
buildSelectedCase(row, kind, ctx) {
|
|
233
|
+
const { projectDir, redactionNotes } = ctx;
|
|
234
|
+
const base = buildCaseFromRow(row, kind, kind === 'negative' ? 'block' : 'allow');
|
|
235
|
+
const redaction = redactParams(base.params, projectDir);
|
|
236
|
+
redactionNotes.push(...redaction.notes);
|
|
237
|
+
const historyResult = readTrajectoryStore(`context history read for session=${row.sessionId} before id=${row.id}`, () => this.db.getRuleHostContextRowsBefore(row.sessionId, row.id, HISTORY_LIMIT));
|
|
238
|
+
const history = assembleBehaviorHistory(historyResult.rows, historyResult.truncated, { projectDir, toolSemantics: this.toolSemantics });
|
|
239
|
+
const action = buildRuleHostAction(row.toolName, redaction.redacted, projectDir, { canonicalKind: this.toolSemantics.resolve(row.toolName) });
|
|
240
|
+
const ruleContext = { version: 2, history, facts: computeBehaviorFacts(history, action.normalizedPath || null, null) };
|
|
241
|
+
return { ...base, params: redaction.redacted, ruleContext };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RuleHistoryWindow, ToolSemanticRegistry } from '@principles/core/runtime-v2';
|
|
2
|
+
import type { RuleHostContextRow } from '@principles/core/trajectory-types';
|
|
3
|
+
export declare function assembleBehaviorHistory(rows: readonly RuleHostContextRow[], truncated: boolean, options: {
|
|
4
|
+
projectDir: string;
|
|
5
|
+
toolSemantics: ToolSemanticRegistry;
|
|
6
|
+
}): RuleHistoryWindow;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** Shared validated history assembly; host vocabulary is supplied by its owner. */
|
|
2
|
+
import { extractFilePathFromParams, normalizePathPure } from '@principles/core/runtime-v2/internalization/rule-host-input-builder';
|
|
3
|
+
const VALID_OUTCOMES = new Set(['success', 'failure', 'blocked']);
|
|
4
|
+
function isPlainObject(value) {
|
|
5
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function isRuleToolOutcome(value) {
|
|
8
|
+
return typeof value === 'string' && VALID_OUTCOMES.has(value);
|
|
9
|
+
}
|
|
10
|
+
function unavailable(reason) {
|
|
11
|
+
return {
|
|
12
|
+
status: 'unavailable',
|
|
13
|
+
unavailableReason: reason,
|
|
14
|
+
truncated: false,
|
|
15
|
+
calls: [],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function assembleBehaviorHistory(rows, truncated, options) {
|
|
19
|
+
const { projectDir, toolSemantics } = options;
|
|
20
|
+
const records = [];
|
|
21
|
+
for (const row of rows) {
|
|
22
|
+
// ── validate tool_name (ERR-001: unknown) ──
|
|
23
|
+
const { toolName } = row;
|
|
24
|
+
if (typeof toolName !== 'string' || toolName.length === 0) {
|
|
25
|
+
return unavailable(`row id=${row.id}: tool_name must be a non-empty string`);
|
|
26
|
+
}
|
|
27
|
+
// ── validate outcome (ERR-001: unknown, enum check) ──
|
|
28
|
+
const { outcome } = row;
|
|
29
|
+
if (!isRuleToolOutcome(outcome)) {
|
|
30
|
+
return unavailable(`row id=${row.id}: outcome "${row.outcome}" is not a valid RuleToolOutcome`);
|
|
31
|
+
}
|
|
32
|
+
// ── validate & parse params_json (ERR-001: unknown) ──
|
|
33
|
+
const paramsJsonStr = row.paramsJson;
|
|
34
|
+
if (typeof paramsJsonStr !== 'string') {
|
|
35
|
+
return unavailable(`row id=${row.id}: params_json must be a string`);
|
|
36
|
+
}
|
|
37
|
+
let parsedParams;
|
|
38
|
+
try {
|
|
39
|
+
parsedParams = JSON.parse(paramsJsonStr);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return unavailable(`row id=${row.id}: params_json is not valid JSON`);
|
|
43
|
+
}
|
|
44
|
+
// Must be a non-array object (spec §5.2)
|
|
45
|
+
if (!isPlainObject(parsedParams)) {
|
|
46
|
+
return unavailable(`row id=${row.id}: params_json must parse to a non-array object`);
|
|
47
|
+
}
|
|
48
|
+
// ── build RuleToolCallRecord ──
|
|
49
|
+
// PRI-634-F: resolve through the supplied registry (baseline + host
|
|
50
|
+
// layer) instead of baseline-only canonicalizeToolKind — production tools
|
|
51
|
+
// like 'shell'/'cmd'/'delete_file' previously degraded to 'other' here
|
|
52
|
+
// while the gate classified them as bash/write (vocabulary drift).
|
|
53
|
+
const canonicalKind = toolSemantics.resolve(toolName);
|
|
54
|
+
// Extract normalized path using the same pure logic as buildRuleHostAction
|
|
55
|
+
// (avoids production/replay drift, spec §4.4)
|
|
56
|
+
const rawPath = extractFilePathFromParams(parsedParams, {
|
|
57
|
+
isBashTool: canonicalKind === 'execute',
|
|
58
|
+
isWriteTool: canonicalKind === 'write',
|
|
59
|
+
toolName,
|
|
60
|
+
});
|
|
61
|
+
const normalizedPath = normalizePathPure(rawPath, projectDir);
|
|
62
|
+
records.push({
|
|
63
|
+
sequenceId: row.id,
|
|
64
|
+
toolName,
|
|
65
|
+
canonicalKind,
|
|
66
|
+
normalizedPath: normalizedPath === '' ? null : normalizedPath,
|
|
67
|
+
paramsSummary: parsedParams,
|
|
68
|
+
outcome,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
status: 'available',
|
|
73
|
+
truncated,
|
|
74
|
+
calls: records,
|
|
75
|
+
};
|
|
76
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -85,3 +85,6 @@ export declare function createProductionHostRuntime(options?: Partial<Pick<HostR
|
|
|
85
85
|
*/
|
|
86
86
|
events?: HostEventEmitter;
|
|
87
87
|
}): HostRuntime;
|
|
88
|
+
export * from './behavior-example-pack-assembler.js';
|
|
89
|
+
export * from './behavior-history.js';
|
|
90
|
+
export * from './workspace-behavior-evidence.js';
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* Hosts inject: a logger, a structured-event sink, their tool catalog, and an
|
|
21
21
|
* env getter. Everything else is the existing production path.
|
|
22
22
|
*/
|
|
23
|
-
import { type ToolSemanticRegistry, type BehaviorEvidencePort } from '@principles/core/runtime-v2';
|
|
23
|
+
import { type ToolSemanticRegistry, type BehaviorEvidencePort, type ArtificerTrustedFactsPort } from '@principles/core/runtime-v2';
|
|
24
24
|
import { type ConsumerGovernanceLogger } from './internalization-consumer-governance.js';
|
|
25
25
|
export declare const INTERNALIZATION_AUTO_CONSUMER_FLAG_ID = "internalization_auto_consumer";
|
|
26
26
|
/** Structural logger port — OpenClaw PluginLogger and worker loggers satisfy this. */
|
|
@@ -71,6 +71,15 @@ export interface InternalizationConsumerCyclePorts {
|
|
|
71
71
|
* and the runner is never constructed evidence-less.
|
|
72
72
|
*/
|
|
73
73
|
readonly behaviorEvidence?: BehaviorEvidencePort;
|
|
74
|
+
/**
|
|
75
|
+
* SPEC-RULECODE-QUALITY-001 A1 (PRI-995): host-injected trusted-facts port
|
|
76
|
+
* for the artificer intent-basis preflight (SPEC 8.3 seam). Optional:
|
|
77
|
+
* absent + a scribe-declared required fact degrades OBSERVABLY
|
|
78
|
+
* (trusted_fact_provider_absent needs_human_review) and the runner is
|
|
79
|
+
* never constructed — a host capability gap never reads as an Owner fault
|
|
80
|
+
* or a transient error.
|
|
81
|
+
*/
|
|
82
|
+
readonly trustedFacts?: ArtificerTrustedFactsPort;
|
|
74
83
|
/** Log label used in human log prefixes ('AutoConsumer' | 'CodexWorker' | …). */
|
|
75
84
|
readonly logLabel: string;
|
|
76
85
|
readonly envGetter?: (name: string) => string | undefined;
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { createRuntimeStateHandle, InternalizationOrchestrator, DreamerRunner, PhilosopherRunner, ScribeRunner, ArtificerRunner, buildArtificerHostSemanticContext, EvaluatorRunner, RolloutReviewerRunner, DefaultDreamerValidator, DefaultPhilosopherValidator, DefaultScribeValidator, DefaultArtificerValidator, DefaultEvaluatorValidator, DefaultRolloutReviewerValidator, PiAiRuntimeAdapter, L2AgentLoopAdapter, ArtificerL2Adapter, buildL2PrincipleReaderFromLedger, OpenClawCliRuntimeAdapter, storeEmitter, createProductionGateDeps, resolveRuntimeConfigFromPdConfig, resolveRuntimeConfigForAgent, AGENT_NAME_FOR_TASK_KIND, isRuntimeConfigError, computeConsumerDecision, FULL_CHAIN_CONSUMER_RUNNER_KINDS, DEFAULT_CONSUMER_RUNNER_KINDS, InternalizationQueueReadModel, MVP_CORE_TASK_KINDS, SqliteConnection, SqliteReconciliationCursorStore, SUCCEEDED_TRANSITIONS_SCOPE, PrincipleTreeLedgerAdapter,
|
|
24
24
|
// PRI-714: resolve outputLanguage from the same effective config the
|
|
25
25
|
// runners already receive (EP-07: canonical resolved value, not raw input).
|
|
26
|
-
resolveOutputLanguage, hydratePITaskRecord, preflightArtificerOwnerEvidence, } from '@principles/core/runtime-v2';
|
|
26
|
+
resolveOutputLanguage, hydratePITaskRecord, preflightArtificerOwnerEvidence, preflightArtificerIntentBasis, consumeSourceFaultBackoffSkip, advanceSourceFaultBackoff, } from '@principles/core/runtime-v2';
|
|
27
27
|
import { loadLedger } from '@principles/core/principle-tree-ledger';
|
|
28
28
|
import { loadPdConfigForPlugin, loadFeatureFlagFromConfig } from './pd-config.js';
|
|
29
29
|
import { createEvaluatorRepairDeps, createRolloutGovernanceDeps } from './internalization-consumer-governance.js';
|
|
@@ -384,6 +384,21 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
|
|
|
384
384
|
preflightSkipReason = 'code_rule_capability_disabled';
|
|
385
385
|
continue;
|
|
386
386
|
}
|
|
387
|
+
// SPEC-RULECODE-QUALITY-001 §9 (Q21): a pending source-fault backoff
|
|
388
|
+
// consumes one skip cycle per encounter — the task stays pending,
|
|
389
|
+
// zero model cost, and the technical retry happens only after the
|
|
390
|
+
// bounded 1-then-2-cycle backoff elapses (restart-durable).
|
|
391
|
+
if (await consumeSourceFaultBackoffSkip(candidate.taskId, leasedPiTask, stateManager)) {
|
|
392
|
+
const backoffInfo = JSON.stringify({
|
|
393
|
+
reason: 'source_fault_backoff_skip',
|
|
394
|
+
taskId: candidate.taskId,
|
|
395
|
+
nextAction: 'Transient source fault backoff in effect; the task retries automatically at zero model cost.',
|
|
396
|
+
});
|
|
397
|
+
emitEvent('INTERNALIZATION_CONSUMER_SKIP', backoffInfo);
|
|
398
|
+
logger.info(`[PD:${logLabel}] Artificer task ${candidate.taskId} skipped this cycle: source-fault backoff.`);
|
|
399
|
+
preflightSkipReason = 'source_fault_backoff_skip';
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
387
402
|
artificerEvidence = await preflightArtificerOwnerEvidence({
|
|
388
403
|
taskId: candidate.taskId,
|
|
389
404
|
piTask: leasedPiTask,
|
|
@@ -415,16 +430,21 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
|
|
|
415
430
|
// assembly at zero model cost), the runner is never constructed,
|
|
416
431
|
// and the preflight already emitted the structured degradation
|
|
417
432
|
// telemetry (reason=evidence_source_error, reviewPersisted=false).
|
|
433
|
+
// SPEC-RULECODE-QUALITY-001 §9 (Q21/Q22): the transient fault now
|
|
434
|
+
// advances the SHARED bounded source-fault backoff (restart-
|
|
435
|
+
// durable counter; 3rd consecutive fault ends auto-retry in a
|
|
436
|
+
// source_fault_persistent review terminal — never an Owner label).
|
|
437
|
+
const persistentFault = await advanceSourceFaultBackoff({ taskId: candidate.taskId, piTask: leasedPiTask, stateManager }, `evidence source error: ${artificerEvidence.detail ?? 'trajectory store I/O fault'}`);
|
|
418
438
|
const storageInfo = JSON.stringify({
|
|
419
|
-
reason: 'evidence_source_error',
|
|
439
|
+
reason: persistentFault ? 'source_fault_persistent' : 'evidence_source_error',
|
|
420
440
|
taskId: candidate.taskId,
|
|
421
441
|
taskKind: candidate.taskKind,
|
|
422
442
|
nextAction: artificerEvidence.nextAction,
|
|
423
443
|
...(artificerEvidence.detail !== undefined ? { detail: artificerEvidence.detail } : {}),
|
|
424
444
|
});
|
|
425
445
|
emitEvent('INTERNALIZATION_CONSUMER_SKIP', storageInfo);
|
|
426
|
-
logger.warn(`[PD:${logLabel}] Artificer task ${candidate.taskId} skipped: evidence source error (storage fault preserved; task stays wake-eligible).`);
|
|
427
|
-
preflightSkipReason = 'evidence_source_error';
|
|
446
|
+
logger.warn(`[PD:${logLabel}] Artificer task ${candidate.taskId} skipped: evidence source error (storage fault preserved; task stays wake-eligible${persistentFault ? '; auto-retry ended after repeated faults' : ''}).`);
|
|
447
|
+
preflightSkipReason = persistentFault ? 'source_fault_persistent' : 'evidence_source_error';
|
|
428
448
|
continue;
|
|
429
449
|
}
|
|
430
450
|
if (artificerEvidence.decision === 'blocked') {
|
|
@@ -444,6 +464,61 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
|
|
|
444
464
|
preflightSkipReason = 'artificer_blocked_on_owner_evidence';
|
|
445
465
|
continue;
|
|
446
466
|
}
|
|
467
|
+
// SPEC-RULECODE-QUALITY-001 A1 (Q01/Q20): intent-basis preflight —
|
|
468
|
+
// evidence-ready is necessary but NOT sufficient. The scribe
|
|
469
|
+
// artifact must carry a valid intentContract AND machine-readable
|
|
470
|
+
// executionConditions, and every declared required trusted fact
|
|
471
|
+
// must be resolvable through ports.trustedFacts. Any stop verdict
|
|
472
|
+
// moves the task to needs_human_review with a stable reasonCode
|
|
473
|
+
// and the runner is NEVER constructed (zero generation calls).
|
|
474
|
+
// The gate re-derives from the scribe artifact every run — a
|
|
475
|
+
// stored verification record is never a pass token.
|
|
476
|
+
const intentBasis = await preflightArtificerIntentBasis({
|
|
477
|
+
taskId: candidate.taskId,
|
|
478
|
+
piTask: leasedPiTask,
|
|
479
|
+
owner,
|
|
480
|
+
workspaceDir,
|
|
481
|
+
stateManager,
|
|
482
|
+
eventEmitter: workspaceEmitter,
|
|
483
|
+
...(ports.trustedFacts !== undefined ? { trustedFacts: ports.trustedFacts } : {}),
|
|
484
|
+
});
|
|
485
|
+
if (intentBasis.decision === 'source_error') {
|
|
486
|
+
// Q21: transient artifact-store fault — the preflight already
|
|
487
|
+
// advanced the shared bounded backoff; task stays wake-eligible.
|
|
488
|
+
emitEvent('INTERNALIZATION_CONSUMER_SKIP', JSON.stringify({
|
|
489
|
+
reason: 'intent_source_error',
|
|
490
|
+
taskId: candidate.taskId,
|
|
491
|
+
nextAction: intentBasis.nextAction,
|
|
492
|
+
detail: intentBasis.detail,
|
|
493
|
+
}));
|
|
494
|
+
logger.warn(`[PD:${logLabel}] Artificer task ${candidate.taskId} skipped: intent-basis source error (transient; bounded backoff applies).`);
|
|
495
|
+
preflightSkipReason = 'intent_source_error';
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (intentBasis.decision === 'source_fault_persistent') {
|
|
499
|
+
emitEvent('INTERNALIZATION_CONSUMER_INTENT_BLOCKED', JSON.stringify({
|
|
500
|
+
taskId: candidate.taskId,
|
|
501
|
+
reasonCode: intentBasis.reasonCode,
|
|
502
|
+
nextAction: intentBasis.nextAction,
|
|
503
|
+
detail: intentBasis.detail,
|
|
504
|
+
reviewPersisted: intentBasis.reviewPersisted,
|
|
505
|
+
}));
|
|
506
|
+
logger.warn(`[PD:${logLabel}] Artificer task ${candidate.taskId} blocked: repeated source faults ended auto-retry (${intentBasis.reasonCode}).`);
|
|
507
|
+
preflightSkipReason = 'source_fault_persistent';
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (intentBasis.decision === 'blocked') {
|
|
511
|
+
emitEvent('INTERNALIZATION_CONSUMER_INTENT_BLOCKED', JSON.stringify({
|
|
512
|
+
taskId: candidate.taskId,
|
|
513
|
+
reasonCode: intentBasis.reasonCode,
|
|
514
|
+
nextAction: intentBasis.nextAction,
|
|
515
|
+
detail: intentBasis.detail,
|
|
516
|
+
reviewPersisted: intentBasis.reviewPersisted,
|
|
517
|
+
}));
|
|
518
|
+
logger.warn(`[PD:${logLabel}] Artificer task ${candidate.taskId} blocked on intent basis (${intentBasis.reasonCode}, reviewPersisted=${intentBasis.reviewPersisted}).`);
|
|
519
|
+
preflightSkipReason = 'artificer_blocked_on_intent_basis';
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
447
522
|
}
|
|
448
523
|
}
|
|
449
524
|
wakeResult = candidate;
|
|
@@ -643,6 +718,12 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
|
|
|
643
718
|
...(artificerEvidence !== null && artificerEvidence.decision === 'ready'
|
|
644
719
|
? { behaviorExamplePack: artificerEvidence.pack }
|
|
645
720
|
: {}),
|
|
721
|
+
// SPEC-RULECODE-QUALITY-001 A1 (Owner review P2): the SAME
|
|
722
|
+
// trusted-facts port the intent preflight used must ride into the
|
|
723
|
+
// runner so the defensive mirror sees the same fact resolutions —
|
|
724
|
+
// without this, a port-resolved task would pass the entry gate and
|
|
725
|
+
// then be refused by the runner's "provider absent" guard.
|
|
726
|
+
...(ports.trustedFacts !== undefined ? { trustedFacts: ports.trustedFacts } : {}),
|
|
646
727
|
},
|
|
647
728
|
// PRI-741: thread the host semantic projection into generation.
|
|
648
729
|
{ ...runnerOptions, ...(artificerHostSemanticContext !== undefined ? { hostSemanticContext: artificerHostSemanticContext } : {}) });
|
|
@@ -85,6 +85,13 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
85
85
|
if (action.normalizedPath === null)
|
|
86
86
|
return { decision: 'allow', source: event.source, metadata: { evaluatedLiveRules: 0 } };
|
|
87
87
|
let context;
|
|
88
|
+
/**
|
|
89
|
+
* SPEC-RULECODE-QUALITY-001 A3 (Q10): the precise reason a v2 rule is
|
|
90
|
+
* suspended — carried into the skip warning so each fault kind (absent /
|
|
91
|
+
* undefined / invalid / failed / unavailable) is distinguishable in host
|
|
92
|
+
* telemetry instead of merging into one opaque 'unavailable'.
|
|
93
|
+
*/
|
|
94
|
+
let contextFault;
|
|
88
95
|
let enrichment = { currentGfi: 0, epTier: 0, bashRisk: 'unknown' };
|
|
89
96
|
const request = {
|
|
90
97
|
workspaceDir: event.context.workspaceDir, sessionId: event.context.sessionId,
|
|
@@ -110,6 +117,7 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
110
117
|
const candidate = await withinGateDeadline(options.ruleContextProvider(request), startedAt);
|
|
111
118
|
if (candidate === undefined) {
|
|
112
119
|
context = undefined;
|
|
120
|
+
contextFault = 'provider_returned_undefined';
|
|
113
121
|
}
|
|
114
122
|
else {
|
|
115
123
|
const validation = validateRuleContextV2(candidate);
|
|
@@ -117,18 +125,27 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
117
125
|
context = candidate;
|
|
118
126
|
else {
|
|
119
127
|
context = UNAVAILABLE_RULE_CONTEXT;
|
|
128
|
+
contextFault = 'provider_invalid';
|
|
120
129
|
addWarning(warnings, `rule_context_invalid: ${validation.errors.join('; ')}`, 'repair the host context provider');
|
|
121
130
|
}
|
|
122
131
|
}
|
|
123
132
|
}
|
|
124
133
|
catch (error) {
|
|
125
134
|
context = UNAVAILABLE_RULE_CONTEXT;
|
|
135
|
+
contextFault = 'provider_failed';
|
|
126
136
|
addWarning(warnings, `rule_context_provider_failed: ${error instanceof Error ? error.message : String(error)}`, 'inspect the host context provider and retry');
|
|
127
137
|
}
|
|
128
138
|
}
|
|
139
|
+
else {
|
|
140
|
+
contextFault = 'provider_absent';
|
|
141
|
+
}
|
|
129
142
|
const dbPath = path.join(event.context.workspaceDir, '.pd', 'state.db');
|
|
130
143
|
if (remainingGateMs(startedAt) <= 0) {
|
|
131
|
-
|
|
144
|
+
// SPEC-RULECODE-QUALITY-001 Q24: a deadline-exhausted allow must be
|
|
145
|
+
// observable as "no rule executed" (evaluatedLiveRules=0), never read
|
|
146
|
+
// as successful governance — isolated probe finding (the early return
|
|
147
|
+
// previously carried no evaluation facts at all).
|
|
148
|
+
return { decision: 'allow', source: event.source, warnings: [boundedWarning('gate_deadline_exceeded', 'inspect host context providers and active RuleCode resource use')], metadata: { evaluatedLiveRules: 0 } };
|
|
132
149
|
}
|
|
133
150
|
if (!fs.existsSync(dbPath)) {
|
|
134
151
|
// Security audit run-1 (gate-failopen-allow-on-state-corruption):
|
|
@@ -308,8 +325,28 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
308
325
|
addWarning(warnings, `unsupported_context_version: ${String(content.requiresContextVersion)}`, 'regenerate the rule artifact for context version 2');
|
|
309
326
|
continue;
|
|
310
327
|
}
|
|
311
|
-
|
|
312
|
-
|
|
328
|
+
// SPEC-RULECODE-QUALITY-001 A3 / §7.1 (Q10): MANDATORY runtime
|
|
329
|
+
// suspension of context-dependent rules. A v2 rule may only
|
|
330
|
+
// execute when the provider resolved a schema-valid context
|
|
331
|
+
// whose history.status is 'available'. The old check (`!context`)
|
|
332
|
+
// let the truthy UNAVAILABLE_RULE_CONTEXT sentinel (provider
|
|
333
|
+
// invalid/thrown) and schema-valid unavailable contexts through,
|
|
334
|
+
// so a v2 rule without its own self-protection still executed on
|
|
335
|
+
// untrusted input. Skipping happens per-call, per-rule — healthy
|
|
336
|
+
// sibling rules (v1 or well-fed v2) keep evaluating, and no
|
|
337
|
+
// governance state changes. Intentional behavior change vs the
|
|
338
|
+
// pre-fix "legal unavailable still executed" path.
|
|
339
|
+
if (context === undefined || context.history.status !== 'available') {
|
|
340
|
+
// Reference identity against the frozen sentinel distinguishes
|
|
341
|
+
// "the provider faulted" from "the provider legitimately
|
|
342
|
+
// declared history unavailable" (same wire shape, different
|
|
343
|
+
// fault class — Q10 asserts them separately).
|
|
344
|
+
const fault = context === undefined
|
|
345
|
+
? (contextFault ?? 'provider_absent')
|
|
346
|
+
: context === UNAVAILABLE_RULE_CONTEXT
|
|
347
|
+
? (contextFault ?? 'provider_invalid')
|
|
348
|
+
: 'history_unavailable';
|
|
349
|
+
addWarning(warnings, `rule_context_v2_unavailable: ${fault} (activation=${activationId})`, 'enable and wire the host rule context provider, or regenerate the rule without context dependence');
|
|
313
350
|
continue;
|
|
314
351
|
}
|
|
315
352
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { BehaviorEvidencePort, ToolSemanticRegistry } from '@principles/core/runtime-v2';
|
|
2
|
+
/** Lazy storage access: missing labels need no DB, and a failed open remains observable. */
|
|
3
|
+
export declare function createWorkspaceBehaviorEvidencePort(workspaceDir: string, toolSemantics?: ToolSemanticRegistry): BehaviorEvidencePort;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Read-only adapter over the canonical trajectory store; never creates/migrates it. */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
import { createOwnerEvidenceScopeError } from '@principles/core/runtime-v2/internalization/evidence-readiness';
|
|
5
|
+
import { BehaviorExamplePackAssembler } from './behavior-example-pack-assembler.js';
|
|
6
|
+
import { resolveWorkspaceHostToolSemantics } from './host-tool-semantic-resolver.js';
|
|
7
|
+
function record(value) {
|
|
8
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function contextRow(value) {
|
|
11
|
+
if (!record(value) || typeof value.id !== 'number' || !Number.isSafeInteger(value.id)
|
|
12
|
+
|| typeof value.tool_name !== 'string' || typeof value.outcome !== 'string' || typeof value.params_json !== 'string') {
|
|
13
|
+
throw new Error('Malformed trajectory context row');
|
|
14
|
+
}
|
|
15
|
+
const params = JSON.parse(value.params_json);
|
|
16
|
+
if (!record(params))
|
|
17
|
+
throw new Error('Malformed trajectory params object');
|
|
18
|
+
if (Object.keys(params).some((key) => key === '__proto__' || key === 'constructor' || key === 'prototype')) {
|
|
19
|
+
throw new Error('Unsafe keys in persisted trajectory params');
|
|
20
|
+
}
|
|
21
|
+
return { id: value.id, toolName: value.tool_name, outcome: value.outcome, paramsJson: value.params_json };
|
|
22
|
+
}
|
|
23
|
+
function source(db) {
|
|
24
|
+
return {
|
|
25
|
+
getPainEventByCanonicalId(id) {
|
|
26
|
+
const row = db.prepare('SELECT id, session_id FROM pain_events WHERE canonical_pain_id = ? LIMIT 1').get(id);
|
|
27
|
+
if (row === undefined)
|
|
28
|
+
return null;
|
|
29
|
+
if (!record(row) || typeof row.id !== 'number' || typeof row.session_id !== 'string')
|
|
30
|
+
throw new Error('Malformed pain lineage row');
|
|
31
|
+
return { id: row.id, sessionId: row.session_id };
|
|
32
|
+
},
|
|
33
|
+
getRuleHostEvidenceRow(id) {
|
|
34
|
+
const row = db.prepare('SELECT id, session_id, tool_name, outcome, params_json FROM tool_calls WHERE id = ? LIMIT 1').get(id);
|
|
35
|
+
if (row === undefined)
|
|
36
|
+
return null;
|
|
37
|
+
if (!record(row) || typeof row.session_id !== 'string')
|
|
38
|
+
throw new Error('Malformed trajectory evidence row');
|
|
39
|
+
return { ...contextRow(row), sessionId: row.session_id };
|
|
40
|
+
},
|
|
41
|
+
getRuleHostContextRowsBefore(sessionId, beforeId, limit) {
|
|
42
|
+
const rows = db.prepare('SELECT id, tool_name, outcome, params_json FROM tool_calls WHERE session_id = ? AND id < ? ORDER BY id DESC LIMIT ?').all(sessionId, beforeId, limit + 1);
|
|
43
|
+
return { rows: rows.slice(0, limit).reverse().map(contextRow), truncated: rows.length > limit };
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Lazy storage access: missing labels need no DB, and a failed open remains observable. */
|
|
48
|
+
export function createWorkspaceBehaviorEvidencePort(workspaceDir, toolSemantics) {
|
|
49
|
+
return {
|
|
50
|
+
assemble(selection) {
|
|
51
|
+
const resolved = toolSemantics === undefined ? resolveWorkspaceHostToolSemantics(workspaceDir) : null;
|
|
52
|
+
if (resolved !== null && !resolved.ok)
|
|
53
|
+
throw createOwnerEvidenceScopeError('evidence_source_error', resolved.reason);
|
|
54
|
+
const semantics = toolSemantics ?? (resolved !== null && resolved.ok ? resolved.registry : undefined);
|
|
55
|
+
if (semantics === undefined)
|
|
56
|
+
throw createOwnerEvidenceScopeError('evidence_source_error', 'Host tool semantics unavailable');
|
|
57
|
+
let db;
|
|
58
|
+
try {
|
|
59
|
+
db = new Database(path.join(workspaceDir, '.state', 'trajectory.db'), { readonly: true, fileMustExist: true });
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
throw createOwnerEvidenceScopeError('evidence_source_error', `Trajectory evidence unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
// One read snapshot prevents a pack combining different trajectory revisions.
|
|
66
|
+
return db.transaction(() => new BehaviorExamplePackAssembler({ source: source(db), toolSemantics: semantics })
|
|
67
|
+
.assemble({ ...selection, projectDir: workspaceDir }))();
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
db.close();
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@principles/host-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -10,7 +10,15 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"default": "./dist/index.js"
|
|
12
12
|
},
|
|
13
|
-
"./package.json": "./package.json"
|
|
13
|
+
"./package.json": "./package.json",
|
|
14
|
+
"./behavior-example-pack-assembler": {
|
|
15
|
+
"types": "./dist/behavior-example-pack-assembler.d.ts",
|
|
16
|
+
"default": "./dist/behavior-example-pack-assembler.js"
|
|
17
|
+
},
|
|
18
|
+
"./behavior-history": {
|
|
19
|
+
"types": "./dist/behavior-history.d.ts",
|
|
20
|
+
"default": "./dist/behavior-history.js"
|
|
21
|
+
}
|
|
14
22
|
},
|
|
15
23
|
"files": [
|
|
16
24
|
"dist",
|
|
@@ -23,8 +31,8 @@
|
|
|
23
31
|
"lint": "eslint \"src/**/*.ts\""
|
|
24
32
|
},
|
|
25
33
|
"dependencies": {
|
|
26
|
-
"@principles/core": "^1.
|
|
27
|
-
"@principles/install-layout": "^0.
|
|
34
|
+
"@principles/core": "^1.293.2",
|
|
35
|
+
"@principles/install-layout": "^0.4.0",
|
|
28
36
|
"better-sqlite3": "^13.0.3",
|
|
29
37
|
"js-yaml": "^5.4.3"
|
|
30
38
|
},
|