@principles/host-runtime 0.7.14 → 0.8.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,7 +1,161 @@
1
- import { RUNTIME_V2_PRINCIPLE_BUDGET, computeFeatureFlagsFromConfig, trimToBudget, } from '@principles/core/runtime-v2';
1
+ import { RUNTIME_V2_PRINCIPLE_BUDGET, computeFeatureFlagsFromConfig, renderPrinciplesToDirectives, trimToBudget, } from '@principles/core/runtime-v2';
2
2
  import { escapeXml } from '@principles/core/prompt-builder';
3
3
  import { loadPdConfigForPlugin } from './pd-config.js';
4
- import { buildActivePrinciplePromptContext, readPromptActivationCandidates } from './active-principle-prompt.js';
4
+ import { buildActivePrinciplePromptContext, readPromptActivationCandidates, MAX_SHARED_DIAGNOSTIC_IDS } from './active-principle-prompt.js';
5
+ import { readWorkspaceHostKindFacts } from './host-liveness-contract.js';
6
+ export const PROMPT_INJECTION_TARGET_HOSTS = ['openclaw', 'codex'];
7
+ export const PROMPT_INJECTION_UNIT = 'utf16_code_units';
8
+ /**
9
+ * THE one route formula (P4): Codex always injects through the shared route
10
+ * (codex-adapter has no flag branch); OpenClaw routes by its
11
+ * `abstraction_layer_v1` gate. Every consumer that needs "which route would
12
+ * host X take" must call this — never re-type the ternary.
13
+ */
14
+ export function promptRouteForHost(hostKind, flagEnabled) {
15
+ if (hostKind === 'codex')
16
+ return 'shared_render';
17
+ return flagEnabled ? 'shared_render' : 'legacy_trim';
18
+ }
19
+ function standardHostRoutes(flagEnabled) {
20
+ return [
21
+ { hostKind: 'openclaw', route: promptRouteForHost('openclaw', flagEnabled) },
22
+ { hostKind: 'codex', route: promptRouteForHost('codex', flagEnabled) },
23
+ ];
24
+ }
25
+ /**
26
+ * Resolve the effective route for this workspace. Pure read of existing
27
+ * config + provenance facts — no new registry, no persisted state.
28
+ */
29
+ export function resolvePromptInjectionRouteDecision(input) {
30
+ const config = loadPdConfigForPlugin(input.workspaceDir);
31
+ const { flags } = computeFeatureFlagsFromConfig(config.effective);
32
+ const flagEnabled = flags.abstraction_layer_v1?.enabled === true;
33
+ const facts = readWorkspaceHostKindFacts(input.workspaceDir);
34
+ if (!config.ok) {
35
+ const [error] = config.errors;
36
+ return {
37
+ status: 'unconfirmed',
38
+ flagEnabled,
39
+ declaredHostKinds: facts.ok ? facts.declaredKinds : [],
40
+ effectiveHostKinds: facts.ok ? facts.effectiveKinds : [],
41
+ ...(!facts.ok ? { hostFactsStatus: 'unreadable' } : {}),
42
+ unconfirmedReason: `config_unreadable: ${error?.reason ?? 'workspace config is invalid'}`,
43
+ nextAction: error?.nextAction ?? 'repair .pd/config.yaml before measuring prompt injection',
44
+ perHost: standardHostRoutes(flagEnabled),
45
+ };
46
+ }
47
+ if (!flags.prompt?.enabled) {
48
+ return {
49
+ status: 'unconfirmed',
50
+ flagEnabled,
51
+ declaredHostKinds: facts.ok ? facts.declaredKinds : [],
52
+ effectiveHostKinds: facts.ok ? facts.effectiveKinds : [],
53
+ ...(!facts.ok ? { hostFactsStatus: 'unreadable' } : {}),
54
+ unconfirmedReason: 'prompt_feature_disabled: the prompt injection surface is off',
55
+ nextAction: 'set features.prompt.enabled=true in .pd/config.yaml',
56
+ perHost: standardHostRoutes(flagEnabled),
57
+ };
58
+ }
59
+ // Explicit request-level host is sufficient to bind the route even when
60
+ // workspace-wide host provenance is unreadable. Config and prompt state
61
+ // above remain mandatory preflight gates.
62
+ if (input.targetHost !== undefined) {
63
+ return {
64
+ status: 'confirmed',
65
+ hostKind: input.targetHost,
66
+ route: promptRouteForHost(input.targetHost, flagEnabled),
67
+ basis: 'explicit_target',
68
+ flagEnabled,
69
+ declaredHostKinds: facts.declaredKinds,
70
+ effectiveHostKinds: facts.ok ? facts.effectiveKinds : [],
71
+ ...(!facts.ok ? { hostFactsStatus: 'unreadable' } : {}),
72
+ };
73
+ }
74
+ if (!facts.ok) {
75
+ return {
76
+ status: 'unconfirmed',
77
+ flagEnabled,
78
+ declaredHostKinds: facts.declaredKinds,
79
+ effectiveHostKinds: [],
80
+ hostFactsStatus: 'unreadable',
81
+ unconfirmedReason: `host_facts_unreadable: ${facts.detail}`,
82
+ nextAction: facts.nextAction,
83
+ perHost: standardHostRoutes(flagEnabled),
84
+ };
85
+ }
86
+ const knownKinds = facts.effectiveKinds.filter((kind) => kind === 'openclaw' || kind === 'codex');
87
+ const unknownKinds = facts.effectiveKinds.filter((kind) => kind !== 'openclaw' && kind !== 'codex');
88
+ if (unknownKinds.length > 0) {
89
+ return {
90
+ status: 'unconfirmed',
91
+ flagEnabled,
92
+ declaredHostKinds: facts.declaredKinds,
93
+ effectiveHostKinds: facts.effectiveKinds,
94
+ unconfirmedReason: `unknown_host_kind_declared: ${unknownKinds.join(', ')} has no known prompt-injection route`,
95
+ nextAction: 'pass an explicit target host (openclaw|codex) for this request, or remove the unknown host declaration',
96
+ perHost: standardHostRoutes(flagEnabled),
97
+ };
98
+ }
99
+ if (knownKinds.length === 0) {
100
+ // No host ever operated this workspace. With the flag ON both standard
101
+ // hosts take the shared route, so the route IS confirmed; with the flag
102
+ // OFF they genuinely differ (OpenClaw list vs Codex shared) and there is
103
+ // no fact to pick one with — report unconfirmed instead of guessing.
104
+ if (flagEnabled) {
105
+ return {
106
+ status: 'confirmed',
107
+ route: 'shared_render',
108
+ basis: 'flag_shared_all_hosts',
109
+ flagEnabled,
110
+ declaredHostKinds: facts.declaredKinds,
111
+ effectiveHostKinds: facts.effectiveKinds,
112
+ };
113
+ }
114
+ return {
115
+ status: 'unconfirmed',
116
+ flagEnabled,
117
+ declaredHostKinds: facts.declaredKinds,
118
+ effectiveHostKinds: facts.effectiveKinds,
119
+ unconfirmedReason: 'no_host_facts_and_flag_off: no host declaration or host evidence exists, and the applicable hosts would take different routes',
120
+ nextAction: 'pass an explicit target host (openclaw|codex), or let a host run once in this workspace so it persists its declaration',
121
+ perHost: standardHostRoutes(flagEnabled),
122
+ };
123
+ }
124
+ if (knownKinds.length === 1) {
125
+ const [maybeHostKind] = knownKinds;
126
+ if (maybeHostKind !== undefined) {
127
+ return {
128
+ status: 'confirmed',
129
+ hostKind: maybeHostKind,
130
+ route: promptRouteForHost(maybeHostKind, flagEnabled),
131
+ basis: 'workspace_host_facts',
132
+ flagEnabled,
133
+ declaredHostKinds: facts.declaredKinds,
134
+ effectiveHostKinds: facts.effectiveKinds,
135
+ };
136
+ }
137
+ }
138
+ // Both openclaw and codex are applicable.
139
+ if (flagEnabled) {
140
+ return {
141
+ status: 'confirmed',
142
+ route: 'shared_render',
143
+ basis: 'flag_shared_all_hosts',
144
+ flagEnabled,
145
+ declaredHostKinds: facts.declaredKinds,
146
+ effectiveHostKinds: facts.effectiveKinds,
147
+ };
148
+ }
149
+ return {
150
+ status: 'unconfirmed',
151
+ flagEnabled,
152
+ declaredHostKinds: facts.declaredKinds,
153
+ effectiveHostKinds: facts.effectiveKinds,
154
+ unconfirmedReason: 'multi_host_routes_differ: openclaw would take the legacy list route and codex the shared route under the current flag-off config',
155
+ nextAction: 'pass an explicit target host (openclaw|codex) for this request, or enable abstraction_layer_v1 so both hosts share one route',
156
+ perHost: standardHostRoutes(flagEnabled),
157
+ };
158
+ }
5
159
  /**
6
160
  * PR-1894: which activations a fair-rotation selector injects across the
7
161
  * whole ring. The plugin's round key advances by one per recorded user turn
@@ -24,18 +178,27 @@ function collectReachableActivationIds(principles, budget) {
24
178
  }
25
179
  return [...reachable];
26
180
  }
181
+ function isDiagnosticCapHit(ids) {
182
+ return ids.length >= MAX_SHARED_DIAGNOSTIC_IDS;
183
+ }
27
184
  /**
28
185
  * PR #1844 follow-up: forecast the prompt-injection budget from the SAME
29
- * projection the agent injection itself uses, routed by the same
30
- * `abstraction_layer_v1` flag the OpenClaw plugin's runtime gate reads:
186
+ * projection the agent injection itself uses:
31
187
  *
32
- * - legacy route (live default, flag OFF): activation candidates from
33
- * `readPromptActivationCandidates` (same FIFO query + artifact resolve as
34
- * the plugin's `PromptActivationReader`) trimmed by the REAL
35
- * `trimToBudget` — identical input × ordering × trim logic as
36
- * `openclaw-plugin/src/hooks/prompt.ts`.
37
- * - shared route (flag ON): delegate to `buildActivePrinciplePromptContext`,
38
- * which IS the production shared path's projection source.
188
+ * - legacy route: activation candidates from `readPromptActivationCandidates`
189
+ * (same FIFO query + artifact resolve as the plugin's list path) trimmed by
190
+ * the REAL `trimToBudget` — identical input × ordering × trim logic as
191
+ * `openclaw-plugin/src/hooks/prompt.ts`;
192
+ * - shared route: delegate to `buildActivePrinciplePromptContext`, which IS
193
+ * the production shared path's projection source (Codex always; OpenClaw
194
+ * when its `abstraction_layer_v1` gate is on).
195
+ *
196
+ * Route selection here is EXPLICIT: `targetHost: 'codex'` forces the shared
197
+ * route (production truth for Codex), `targetHost: 'openclaw'` follows the
198
+ * workspace flag, and no target follows the flag (the historical behavior —
199
+ * callers that lack host context should prefer
200
+ * `resolveLivePromptInjectionProjection`, which binds the route to real host
201
+ * facts and refuses to guess).
39
202
  *
40
203
  * Known conservative gap: the plugin's legacy route additionally dedups
41
204
  * candidates against the legacy evolution ledger before trimming; the
@@ -43,19 +206,21 @@ function collectReachableActivationIds(principles, budget) {
43
206
  * forecast slightly MORE consumption than the real injection (never less).
44
207
  *
45
208
  * PR-1894 — round-key alignment. The plugin derives its fair-rotation round
46
- * key from the CURRENT session's turn ordinal
47
- * (`nextSessionTurnOrdinal`), which the console has no access to. Passing no
48
- * key therefore does NOT make the forecast "more conservative": it selects a
49
- * DIFFERENT policy (`legacy_fifo_prefix_v1`) than the one production runs, and
50
- * legacy FIFO structurally excludes the newest activation from every
51
- * truncated selection. The console must either supply the round key or
52
- * report the policy honestly, so this function takes the key as an explicit
53
- * input and exposes `selectionPolicy` on the result.
209
+ * key from the CURRENT session's turn ordinal (`nextSessionTurnOrdinal`),
210
+ * which the console has no access to. Passing no key therefore does NOT make
211
+ * the forecast "more conservative": it selects a DIFFERENT policy
212
+ * (`legacy_fifo_prefix_v1`) than the one production runs. The console must
213
+ * either supply the round key or report the policy honestly, so this
214
+ * function takes the key as an explicit input and exposes
215
+ * `selectionPolicy` on the result.
54
216
  */
55
217
  export async function buildLivePromptInjectionProjection(input) {
56
- const sharedRoute = computeFeatureFlagsFromConfig(loadPdConfigForPlugin(input.workspaceDir).effective).flags.abstraction_layer_v1?.enabled === true;
218
+ const flagEnabled = computeFeatureFlagsFromConfig(loadPdConfigForPlugin(input.workspaceDir).effective).flags.abstraction_layer_v1?.enabled === true;
219
+ const sharedRoute = input.targetHost === 'codex' ? true : flagEnabled;
57
220
  if (sharedRoute) {
58
221
  const context = await buildActivePrinciplePromptContext(input);
222
+ const oversized = context.oversizedActivationIds ?? [];
223
+ const dropped = context.droppedActivationIds ?? [];
59
224
  return {
60
225
  route: 'shared_render',
61
226
  budget: context.budget,
@@ -71,6 +236,12 @@ export async function buildLivePromptInjectionProjection(input) {
71
236
  eventuallyInjectedActivationIds: [],
72
237
  eligibleCount: context.eligibleCount ?? 0,
73
238
  productionRotates: false,
239
+ unit: PROMPT_INJECTION_UNIT,
240
+ budgetScope: 'full_directive_context',
241
+ oversizedActivationIds: oversized,
242
+ droppedActivationIds: dropped,
243
+ oversizedDiagnosticTruncated: isDiagnosticCapHit(oversized),
244
+ droppedDiagnosticTruncated: isDiagnosticCapHit(dropped),
74
245
  };
75
246
  }
76
247
  const candidates = await readPromptActivationCandidates(input);
@@ -88,6 +259,12 @@ export async function buildLivePromptInjectionProjection(input) {
88
259
  eventuallyInjectedActivationIds: [],
89
260
  eligibleCount: 0,
90
261
  productionRotates: false,
262
+ unit: PROMPT_INJECTION_UNIT,
263
+ budgetScope: 'selected_lines',
264
+ oversizedActivationIds: [],
265
+ droppedActivationIds: [],
266
+ oversizedDiagnosticTruncated: false,
267
+ droppedDiagnosticTruncated: false,
91
268
  };
92
269
  }
93
270
  // PR-1894: pass the caller's round key so the forecast runs the SAME
@@ -101,6 +278,7 @@ export async function buildLivePromptInjectionProjection(input) {
101
278
  // "queued behind rotation" apart from "structurally starved", otherwise it
102
279
  // keeps telling the Owner to deactivate healthy principles.
103
280
  const eventuallyInjectedActivationIds = collectReachableActivationIds(principles, RUNTIME_V2_PRINCIPLE_BUDGET);
281
+ const fullRenderChars = renderPrinciplesToDirectives(injected, trimmed.injectedIds, { escapeFn: escapeXml, selfReportInstruction: candidates.selfReportEnabled }).length;
104
282
  return {
105
283
  route: 'legacy_trim',
106
284
  budget: RUNTIME_V2_PRINCIPLE_BUDGET,
@@ -113,5 +291,31 @@ export async function buildLivePromptInjectionProjection(input) {
113
291
  eventuallyInjectedActivationIds,
114
292
  eligibleCount: trimmed.eligibleCount,
115
293
  productionRotates: true,
294
+ unit: PROMPT_INJECTION_UNIT,
295
+ budgetScope: 'selected_lines',
296
+ oversizedActivationIds: trimmed.oversizedActivationIds,
297
+ droppedActivationIds: trimmed.droppedActivationIds,
298
+ oversizedDiagnosticTruncated: isDiagnosticCapHit(trimmed.oversizedActivationIds),
299
+ droppedDiagnosticTruncated: isDiagnosticCapHit(trimmed.droppedActivationIds),
300
+ ...(injected.length > 0 ? { fullRenderChars } : {}),
116
301
  };
117
302
  }
303
+ export async function resolveLivePromptInjectionProjection(input) {
304
+ const decision = resolvePromptInjectionRouteDecision(input);
305
+ if (decision.status === 'confirmed') {
306
+ const projection = await buildLivePromptInjectionProjection({
307
+ ...input,
308
+ targetHost: decision.hostKind,
309
+ });
310
+ return { status: 'confirmed', decision, projection };
311
+ }
312
+ const hosts = decision.perHost?.map((entry) => entry.hostKind) ?? [...PROMPT_INJECTION_TARGET_HOSTS];
313
+ const perHost = [];
314
+ for (const hostKind of hosts) {
315
+ perHost.push({
316
+ hostKind,
317
+ projection: await buildLivePromptInjectionProjection({ ...input, targetHost: hostKind }),
318
+ });
319
+ }
320
+ return { status: 'unconfirmed', decision, perHost };
321
+ }
@@ -0,0 +1,44 @@
1
+ import { type PromptInjectionBudgetScope, type PromptInjectionRouteDecision, type PromptInjectionRouteName, type PromptInjectionTargetHost } from './prompt-injection-projection.js';
2
+ /**
3
+ * SPEC PD_PROMPT_CAPACITY_V1 R-A3: per-live-activation injection status for
4
+ * the activations surface. Every row carries POSITIVE single-item evidence
5
+ * computed with the production serializer for the bound route — "oversized"
6
+ * means this row's own serialized entry exceeds the budget, "window_excluded"
7
+ * means it fits but the current window is full (AC-06: a row is never deemed
8
+ * healthy merely because it is absent from a bounded diagnostic list), and
9
+ * `route_unconfirmed` surfaces AC-01's honest unknown instead of a guess.
10
+ * No compliance/遵守 counts are produced here — absence of injection logs is
11
+ * NOT reported as "zero compliance" (AC-12 boundary).
12
+ */
13
+ export type PromptActivationInjectionStatusKind = 'injectable' | 'oversized' | 'window_excluded' | 'resolution_failed' | 'route_unconfirmed';
14
+ export interface PromptActivationInjectionStatus {
15
+ activationId: string;
16
+ principleId?: string;
17
+ status: PromptActivationInjectionStatusKind;
18
+ /** Present for injectable/window_excluded/oversized under a confirmed route. */
19
+ inCurrentWindow?: boolean;
20
+ /** Present when the single-item cost was computable. */
21
+ costChars?: number;
22
+ budget?: number;
23
+ budgetScope?: PromptInjectionBudgetScope;
24
+ route?: PromptInjectionRouteName;
25
+ /** Present for resolution_failed / route_unconfirmed (structured reason). */
26
+ reason?: string;
27
+ /** Present for route_unconfirmed: per-host single-item fit facts. */
28
+ perHostFits?: readonly {
29
+ hostKind: PromptInjectionTargetHost;
30
+ fits: boolean;
31
+ }[];
32
+ }
33
+ export interface PromptActivationInjectionStatusReport {
34
+ route: PromptInjectionRouteDecision;
35
+ /** Empty when injection is globally unavailable (prompt flag off / state.db missing) — see `globalReason`. */
36
+ statuses: PromptActivationInjectionStatus[];
37
+ warnings: string[];
38
+ /** Present when no status could be computed at all (fail-loud degradation, rc-9). */
39
+ globalReason?: string;
40
+ }
41
+ export declare function readPromptActivationInjectionStatuses(input: {
42
+ workspaceDir: string;
43
+ targetHost?: PromptInjectionTargetHost;
44
+ }): Promise<PromptActivationInjectionStatusReport>;
@@ -0,0 +1,147 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { RUNTIME_V2_PRINCIPLE_BUDGET, SqliteActivationStateStore, SqliteConnection, SqlitePIArtifactStore, computeFeatureFlagsFromConfig, filterPromptActivations, resolvePrincipleFromArtifact, } from '@principles/core/runtime-v2';
4
+ import { loadPdConfigForPlugin } from './pd-config.js';
5
+ import { computePromptSingleItemFit } from './prompt-activation-deliverability.js';
6
+ import { PROMPT_INJECTION_TARGET_HOSTS, buildLivePromptInjectionProjection, promptRouteForHost, resolvePromptInjectionRouteDecision, } from './prompt-injection-projection.js';
7
+ export async function readPromptActivationInjectionStatuses(input) {
8
+ const config = loadPdConfigForPlugin(input.workspaceDir);
9
+ const warnings = [];
10
+ if (!config.ok) {
11
+ warnings.push(...config.errors.map((error) => `config_invalid: ${error.reason}; nextAction=${error.nextAction}`));
12
+ }
13
+ const { flags } = computeFeatureFlagsFromConfig(config.effective);
14
+ const selfReportEnabled = flags.principle_receipt_self_report?.enabled === true;
15
+ const route = resolvePromptInjectionRouteDecision(input);
16
+ if (!flags.prompt?.enabled) {
17
+ return {
18
+ route,
19
+ statuses: [],
20
+ warnings,
21
+ globalReason: 'prompt_feature_disabled: the prompt injection surface is off, so no activation can be injected; nextAction=set features.prompt.enabled=true in .pd/config.yaml',
22
+ };
23
+ }
24
+ const stateDbPath = path.join(input.workspaceDir, '.pd', 'state.db');
25
+ if (!fs.existsSync(stateDbPath)) {
26
+ return {
27
+ route,
28
+ statuses: [],
29
+ warnings,
30
+ globalReason: 'activation_db_not_found: .pd/state.db is missing, live activations cannot be read; nextAction=initialize_workspace_runtime_state',
31
+ };
32
+ }
33
+ const rows = [];
34
+ let connection;
35
+ try {
36
+ connection = new SqliteConnection({ workspaceDir: input.workspaceDir, readonly: true, bootstrapIfMissing: false });
37
+ const activations = filterPromptActivations(await new SqliteActivationStateStore(connection).listPromptActivations());
38
+ const artifactStore = new SqlitePIArtifactStore(connection);
39
+ for (const activation of activations) {
40
+ let artifact;
41
+ try {
42
+ artifact = await artifactStore.getArtifactById(activation.artifactId);
43
+ }
44
+ catch (error) {
45
+ const message = error instanceof Error ? error.message : String(error);
46
+ rows.push({ activationId: activation.activationId, failure: `artifact_query_failed: ${message}` });
47
+ continue;
48
+ }
49
+ if (artifact === null) {
50
+ rows.push({ activationId: activation.activationId, failure: `artifact_not_found: artifactId=${activation.artifactId}` });
51
+ continue;
52
+ }
53
+ const resolved = resolvePrincipleFromArtifact({
54
+ artifact_id: artifact.artifactId,
55
+ artifact_kind: artifact.artifactKind,
56
+ content_json: artifact.contentJson,
57
+ validation_status: artifact.validationStatus,
58
+ }, activation);
59
+ if (!resolved.ok) {
60
+ rows.push({ activationId: activation.activationId, failure: resolved.warning });
61
+ continue;
62
+ }
63
+ rows.push({
64
+ activationId: activation.activationId,
65
+ principleId: resolved.principle.principleId,
66
+ text: resolved.principle.text,
67
+ });
68
+ }
69
+ }
70
+ catch (error) {
71
+ const message = error instanceof Error ? error.message : String(error);
72
+ return {
73
+ route,
74
+ statuses: [],
75
+ warnings,
76
+ globalReason: `activation_db_unreadable: ${message}; nextAction=check_workspace_pd_state_db`,
77
+ };
78
+ }
79
+ finally {
80
+ connection?.close();
81
+ }
82
+ if (route.status === 'unconfirmed') {
83
+ const statuses = rows.map((row) => {
84
+ if ('failure' in row) {
85
+ return { activationId: row.activationId, status: 'resolution_failed', reason: row.failure };
86
+ }
87
+ const perHostFits = PROMPT_INJECTION_TARGET_HOSTS.map((hostKind) => {
88
+ const hostRoute = promptRouteForHost(hostKind, route.flagEnabled);
89
+ const { fits } = computePromptSingleItemFit({
90
+ principle: { principleId: row.principleId, text: row.text, artifactId: '', activationId: row.activationId },
91
+ route: hostRoute,
92
+ selfReportEnabled,
93
+ });
94
+ return { hostKind, fits };
95
+ });
96
+ return {
97
+ activationId: row.activationId,
98
+ principleId: row.principleId,
99
+ status: 'route_unconfirmed',
100
+ reason: route.unconfirmedReason ?? 'effective injection route cannot be confirmed',
101
+ perHostFits,
102
+ };
103
+ });
104
+ return { route, statuses, warnings };
105
+ }
106
+ const projection = await buildLivePromptInjectionProjection({
107
+ workspaceDir: input.workspaceDir,
108
+ targetHost: route.hostKind,
109
+ });
110
+ warnings.push(...projection.warnings);
111
+ const statuses = rows.map((row) => {
112
+ if ('failure' in row) {
113
+ return { activationId: row.activationId, status: 'resolution_failed', reason: row.failure };
114
+ }
115
+ const { fits, costChars } = computePromptSingleItemFit({
116
+ principle: { principleId: row.principleId, text: row.text, artifactId: '', activationId: row.activationId },
117
+ route: projection.route,
118
+ selfReportEnabled,
119
+ });
120
+ const inCurrentWindow = projection.injectedActivationIds.includes(row.activationId);
121
+ const budgetScope = projection.budgetScope;
122
+ if (!fits) {
123
+ return {
124
+ activationId: row.activationId,
125
+ principleId: row.principleId,
126
+ status: 'oversized',
127
+ inCurrentWindow: false,
128
+ costChars,
129
+ budget: RUNTIME_V2_PRINCIPLE_BUDGET,
130
+ budgetScope,
131
+ route: projection.route,
132
+ reason: `single_item_exceeds_budget: this principle's own entry is ${costChars} chars > ${RUNTIME_V2_PRINCIPLE_BUDGET} under the ${projection.route === 'shared_render' ? 'full directive render' : 'list entry'} serialization`,
133
+ };
134
+ }
135
+ return {
136
+ activationId: row.activationId,
137
+ principleId: row.principleId,
138
+ status: inCurrentWindow ? 'injectable' : 'window_excluded',
139
+ inCurrentWindow,
140
+ costChars,
141
+ budget: RUNTIME_V2_PRINCIPLE_BUDGET,
142
+ budgetScope,
143
+ route: projection.route,
144
+ };
145
+ });
146
+ return { route, statuses, warnings };
147
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/host-runtime",
3
- "version": "0.7.14",
3
+ "version": "0.8.0",
4
4
  "description": "Shared host-neutral orchestration for Principles Disciple MVP-Core hook paths.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,7 +21,7 @@
21
21
  "lint": "eslint \"src/**/*.ts\""
22
22
  },
23
23
  "dependencies": {
24
- "@principles/core": "^1.289.1",
24
+ "@principles/core": "^1.290.0",
25
25
  "@principles/install-layout": "^0.2.7",
26
26
  "better-sqlite3": "^13.0.3",
27
27
  "js-yaml": "^5.4.1"