@principles/host-runtime 0.7.13 → 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,4 +1,6 @@
1
1
  import { type ActivatedPrinciple, type PromptSelectionPolicy } from '@principles/core/runtime-v2';
2
+ /** PRI-904 SPEC §10: bounded diagnostic id lists (mirrors the core selector's cap of 16). */
3
+ export declare const MAX_SHARED_DIAGNOSTIC_IDS = 16;
2
4
  export interface ActivePrinciplePromptResult {
3
5
  additionalContext: string;
4
6
  principleIds: string[];
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import { escapeXml } from '@principles/core/prompt-builder';
5
5
  import { loadPdConfigForPlugin } from './pd-config.js';
6
6
  /** PRI-904 SPEC §10: bounded diagnostic id lists (mirrors the core selector's cap of 16). */
7
- const MAX_SHARED_DIAGNOSTIC_IDS = 16;
7
+ export const MAX_SHARED_DIAGNOSTIC_IDS = 16;
8
8
  /**
9
9
  * PR #1844 follow-up: the activation READ half of buildActivePrinciplePromptContext,
10
10
  * exported so the console budget projection can share the exact same input
@@ -23,6 +23,33 @@ export type PromotionHostLivenessResolution = {
23
23
  hostRuntimeVersion: null;
24
24
  nextAction: string;
25
25
  };
26
+ /**
27
+ * Workspace host-kind facts shared by every host-aware projection (prompt
28
+ * injection route resolution reuses the same two durable provenance sources
29
+ * as promotion liveness — one truth, no second registry):
30
+ * - `declaredKinds`: host-authored declarations under
31
+ * `.pd/host-tool-semantics/<hostKind>.json` (each host writes its own file
32
+ * on startup);
33
+ * - `evidenceKinds`: distinct `pain_events.host_kind` values in
34
+ * trajectory.db (which hosts actually produced governance evidence here —
35
+ * survives declaration deletion); pre-column tables carry no evidence by
36
+ * construction;
37
+ * - `effectiveKinds`: union of both, sorted, deduplicated.
38
+ */
39
+ export type WorkspaceHostKindFacts = {
40
+ readonly ok: true;
41
+ readonly declaredKinds: readonly string[];
42
+ readonly evidenceKinds: readonly string[];
43
+ readonly effectiveKinds: readonly string[];
44
+ } | {
45
+ readonly ok: false;
46
+ readonly reason: 'host_declarations_unreadable' | 'workspace_provenance_unreadable';
47
+ /** Declarations that DID parse before the failure (empty when declarations themselves failed). */
48
+ readonly declaredKinds: readonly string[];
49
+ readonly detail: string;
50
+ readonly nextAction: string;
51
+ };
52
+ export declare function readWorkspaceHostKindFacts(workspaceDir: string): WorkspaceHostKindFacts;
26
53
  /**
27
54
  * PRI-813: resolve the promotion host-liveness contract from the workspace's
28
55
  * REAL host state instead of unconditionally inheriting the OpenClaw
@@ -72,6 +72,34 @@ function readWorkspaceHostProvenance(workspaceDir) {
72
72
  function promotionHostFailure(reason, hostKinds, nextAction) {
73
73
  return { ok: false, reason, hostKinds, hostContract: null, hostRuntimeVersion: null, nextAction };
74
74
  }
75
+ export function readWorkspaceHostKindFacts(workspaceDir) {
76
+ const loaded = loadHostToolDeclarations(workspaceDir);
77
+ if (!loaded.ok && loaded.reason !== 'host_tool_declaration_missing') {
78
+ return {
79
+ ok: false,
80
+ reason: 'host_declarations_unreadable',
81
+ declaredKinds: [],
82
+ detail: `${loaded.reason} (${loaded.nextAction})`,
83
+ nextAction: `repair the host tool declarations before relying on host facts: ${loaded.reason} (${loaded.nextAction})`,
84
+ };
85
+ }
86
+ const declaredKinds = loaded.ok
87
+ ? [...new Set(loaded.declarations.map((declaration) => declaration.hostKind))].sort()
88
+ : [];
89
+ const provenance = readWorkspaceHostProvenance(workspaceDir);
90
+ if (!provenance.ok) {
91
+ return {
92
+ ok: false,
93
+ reason: 'workspace_provenance_unreadable',
94
+ declaredKinds,
95
+ detail: provenance.nextAction,
96
+ nextAction: provenance.nextAction,
97
+ };
98
+ }
99
+ const evidenceKinds = provenance.kinds.filter((kind) => kind === 'openclaw' || kind === 'codex');
100
+ const effectiveKinds = [...new Set([...declaredKinds, ...evidenceKinds])].sort();
101
+ return { ok: true, declaredKinds, evidenceKinds, effectiveKinds };
102
+ }
75
103
  /**
76
104
  * PRI-813: resolve the promotion host-liveness contract from the workspace's
77
105
  * REAL host state instead of unconditionally inheriting the OpenClaw
@@ -107,19 +135,17 @@ function promotionHostFailure(reason, hostKinds, nextAction) {
107
135
  * truthfully over state that already exists.
108
136
  */
109
137
  export function resolvePromotionHostLiveness(workspaceDir) {
110
- const loaded = loadHostToolDeclarations(workspaceDir);
111
- if (!loaded.ok && loaded.reason !== 'host_tool_declaration_missing') {
112
- return promotionHostFailure('host_declarations_unreadable', [], `repair the host tool declarations before promoting: ${loaded.reason} (${loaded.nextAction})`);
113
- }
114
- const declaredKinds = loaded.ok
115
- ? [...new Set(loaded.declarations.map(declaration => declaration.hostKind))].sort()
116
- : [];
117
- const provenance = readWorkspaceHostProvenance(workspaceDir);
118
- if (!provenance.ok) {
119
- return promotionHostFailure('workspace_provenance_unreadable', declaredKinds, provenance.nextAction);
138
+ // Same two provenance sources and failure semantics as before, now via the
139
+ // shared facts reader (readWorkspaceHostKindFacts) so prompt-injection
140
+ // route resolution and promotion liveness cannot drift apart.
141
+ const facts = readWorkspaceHostKindFacts(workspaceDir);
142
+ if (!facts.ok) {
143
+ return promotionHostFailure(facts.reason, facts.declaredKinds, facts.reason === 'host_declarations_unreadable'
144
+ ? `repair the host tool declarations before promoting: ${facts.detail}`
145
+ : facts.nextAction);
120
146
  }
121
- const provenanceKinds = provenance.kinds.filter(kind => kind === 'openclaw' || kind === 'codex');
122
- const effectiveKinds = [...new Set([...declaredKinds, ...provenanceKinds])].sort();
147
+ const { declaredKinds } = facts;
148
+ const { effectiveKinds } = facts;
123
149
  // The promotion host can only be the OpenClaw contract when nothing on the
124
150
  // workspace — neither a declaration nor behavioral evidence — says Codex.
125
151
  if (!effectiveKinds.includes('codex')) {
package/dist/index.d.ts CHANGED
@@ -6,6 +6,9 @@ import { type PainDatabaseFactory, type PainEnrichmentProvider } from './product
6
6
  import type { GovernanceHostKind, ToolSemanticRegistry } from '@principles/core/runtime-v2';
7
7
  export * from './active-principle-prompt.js';
8
8
  export * from './prompt-injection-projection.js';
9
+ export * from './prompt-activation-deliverability.js';
10
+ export * from './prompt-injection-status.js';
11
+ export * from './prompt-injection-event-evidence.js';
9
12
  export * from './pd-config.js';
10
13
  export * from './production-rulehost-gate.js';
11
14
  export * from './rule-implementation-runtime.js';
@@ -30,6 +33,7 @@ export * from './codex-ingestion-consent.js';
30
33
  export * from './codex-worker-status.js';
31
34
  export * from './codex-legacy-registration.js';
32
35
  export * from './codex-transcript-locate.js';
36
+ export * from './intervention-evidence-ingress.js';
33
37
  export declare const HOST_RUNTIME_ROUTES: readonly ['before_prompt_build', 'before_tool_call', 'after_tool_call'];
34
38
  export type HostRuntimeRoute = (typeof HOST_RUNTIME_ROUTES)[number];
35
39
  export type HostRuntimePort = (event: HostEvent) => HostEventResult | Promise<HostEventResult>;
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { isHostEvent, isHostEventResult, } from '@principles/core/host';
2
3
  import { RUNTIME_V2_PRINCIPLE_BUDGET } from '@principles/core/runtime-v2';
3
4
  import { buildActivePrinciplePromptContext } from './active-principle-prompt.js';
@@ -5,6 +6,15 @@ import { createProductionRuleHostGate } from './production-rulehost-gate.js';
5
6
  import { createProductionPainEvidenceHandler } from './production-pain-evidence.js';
6
7
  export * from './active-principle-prompt.js';
7
8
  export * from './prompt-injection-projection.js';
9
+ // SPEC PD_PROMPT_CAPACITY_V1: route-aware single-artifact deliverability
10
+ // precheck (B1 write gate + A candidate fit) and the per-live-activation
11
+ // injection status reader (A activation list). ONE implementation shared by
12
+ // Console and CLI — no per-entry cost logic in either consumer.
13
+ export * from './prompt-activation-deliverability.js';
14
+ export * from './prompt-injection-status.js';
15
+ // SPEC PD_PROMPT_CAPACITY_V1 Phase C (AC-12): honest read-model over the
16
+ // existing injection-event JSONL — unknown vs proven-zero never conflated.
17
+ export * from './prompt-injection-event-evidence.js';
8
18
  export * from './pd-config.js';
9
19
  export * from './production-rulehost-gate.js';
10
20
  export * from './rule-implementation-runtime.js';
@@ -49,6 +59,10 @@ export * from './codex-legacy-registration.js';
49
59
  // PRI-625 Slice D: transcript locator moved here so the §15 health service
50
60
  // computes per-rollout lag with the SAME locator the catch-up path uses.
51
61
  export * from './codex-transcript-locate.js';
62
+ // PD v2 Phase 1: shared intervention evidence ingress — the ONE validated
63
+ // entry point every host source (OpenClaw hooks, Codex pd-hook) uses to
64
+ // append normalized evidence. Never throws; never delays a hook.
65
+ export * from './intervention-evidence-ingress.js';
52
66
  export const HOST_RUNTIME_ROUTES = [
53
67
  'before_prompt_build',
54
68
  'before_tool_call',
@@ -212,6 +226,8 @@ export function createProductionHostRuntime(options = {}) {
212
226
  skippedWarnings: prompt.warnings,
213
227
  injectedCharCount: prompt.additionalContext.length,
214
228
  budget: RUNTIME_V2_PRINCIPLE_BUDGET,
229
+ ...(options.hostKind === 'openclaw' || options.hostKind === 'codex' ? { hostKind: options.hostKind } : {}),
230
+ eventId: randomUUID(),
215
231
  ...(prompt.truncated !== undefined ? { v2Truncated: prompt.truncated } : {}),
216
232
  ...(event.context.turnId !== undefined ? { runId: event.context.turnId } : {}),
217
233
  // PRI-904: selection diagnostics (bounded, optional). On this route
@@ -0,0 +1,41 @@
1
+ import { type InterventionActivationOccurrenceRef, type InterventionContentRef, type InterventionEvidenceBatchInput } from '@principles/core/runtime-v2';
2
+ export interface InterventionIngressAppendResult {
3
+ ok: boolean;
4
+ insertedCount?: number;
5
+ duplicateCount?: number;
6
+ conflictCount?: number;
7
+ reason?: string;
8
+ nextAction?: string;
9
+ }
10
+ export interface ExistingInterventionObservationSource {
11
+ sourceKind: 'openclaw_plugin_event_log' | 'codex_pd_hook_event_log';
12
+ sourceLocator: string;
13
+ observationKey: string;
14
+ principleId: string;
15
+ activationId: string;
16
+ artifactId?: string;
17
+ }
18
+ export interface InterventionEvidenceIngress {
19
+ /** Best-effort normalized append. Never throws. */
20
+ appendObservationBatch(input: {
21
+ workspaceDir: string;
22
+ batch: InterventionEvidenceBatchInput;
23
+ }): InterventionIngressAppendResult;
24
+ /** Evidence scope id for a governance workspace (stable per workspace). */
25
+ evidenceScopeIdFor(workspaceDir: string): string;
26
+ /** Digest the actual governed artifact content, not activation metadata. */
27
+ resolveInterventionContentRef(workspaceDir: string, activationRef: InterventionActivationOccurrenceRef, principleId: string): InterventionContentRef | null;
28
+ /**
29
+ * Resolve the activation occurrence metadata snapshot. The snapshot digest
30
+ * is not a Principle content digest; use resolveInterventionContentRef for
31
+ * the artifact's actual bytes.
32
+ */
33
+ resolveActivationOccurrenceRef(workspaceDir: string, activationId: string): InterventionActivationOccurrenceRef | null;
34
+ /** Reuse the exact references already persisted for an idempotent source key. */
35
+ existingObservationRefs(workspaceDir: string, source: ExistingInterventionObservationSource): {
36
+ activationRef: InterventionActivationOccurrenceRef;
37
+ contentRef: InterventionContentRef;
38
+ } | null;
39
+ }
40
+ export declare function createInterventionEvidenceIngress(): InterventionEvidenceIngress;
41
+ export declare function getInterventionEvidenceIngress(): InterventionEvidenceIngress;
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Intervention Evidence Ingress — the single normalized entry point every
3
+ * runtime source uses to append intervention evidence (PD v2 Phase 1,
4
+ * ADR-0027 §2.3; SPEC §13.7 "统一入口").
5
+ *
6
+ * Host adapters build observation batches; ONLY this module validates,
7
+ * normalizes and persists them. No adapter writes evidence tables directly.
8
+ *
9
+ * Failure contract (SPEC §13.7): evidence persistence failure must NEVER
10
+ * change an already computed tool allow/deny. The ingress never throws —
11
+ * every failure returns a structured result with reason + nextAction, and
12
+ * the durable raw source (event log / receipt ledger rows) stays available
13
+ * for bounded replay. The writer connection uses busyTimeoutMs: 0, so a
14
+ * contended state.db lock surfaces immediately as storage_unavailable
15
+ * instead of delaying a hook.
16
+ *
17
+ * Scope identity (SPEC §13.3): evidence_scope_id is persisted in the local
18
+ * evidence ledger and reused after workspace relocation. A canonical path
19
+ * only provides a deterministic candidate when a ledger has no scope yet.
20
+ */
21
+ import { createHash } from 'node:crypto';
22
+ import { isAbsolute, resolve } from 'node:path';
23
+ import { realpathSync } from 'node:fs';
24
+ import { normalizeInterventionEvidenceBatch, SqliteConnection, SqliteInterventionEvidenceStore, } from '@principles/core/runtime-v2';
25
+ function isRecord(value) {
26
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
27
+ }
28
+ export function createInterventionEvidenceIngress() {
29
+ function scopeIdCandidateFor(workspaceDir) {
30
+ const absolute = isAbsolute(workspaceDir) ? resolve(workspaceDir) : resolve(process.cwd(), workspaceDir);
31
+ // Resolve aliases before using the path as a first-append candidate. Once
32
+ // persisted, the database scope remains authoritative across relocations.
33
+ let resolved = absolute;
34
+ try {
35
+ resolved = realpathSync(absolute);
36
+ }
37
+ catch { /* keep absolute */ }
38
+ return `pd-evidence-scope:sha256:${createHash('sha256').update(resolved, 'utf8').digest('hex').slice(0, 32)}`;
39
+ }
40
+ function evidenceScopeIdFor(workspaceDir) {
41
+ const candidate = scopeIdCandidateFor(workspaceDir);
42
+ let connection = null;
43
+ try {
44
+ connection = new SqliteConnection({ workspaceDir, readonly: true, bootstrapIfMissing: false });
45
+ const established = new SqliteInterventionEvidenceStore(connection).getEvidenceScope();
46
+ if (established)
47
+ return established.scopeId;
48
+ }
49
+ catch {
50
+ // A missing or unreadable read-only database cannot confirm a persisted
51
+ // scope; return an uncached candidate and let the writer recheck.
52
+ }
53
+ finally {
54
+ try {
55
+ connection?.close();
56
+ }
57
+ catch { /* best-effort */ }
58
+ }
59
+ return candidate;
60
+ }
61
+ function resolveInterventionContentRef(workspaceDir, activationRef, principleId) {
62
+ if (!activationRef.artifactId)
63
+ return null;
64
+ let connection = null;
65
+ try {
66
+ connection = new SqliteConnection({ workspaceDir, readonly: true, bootstrapIfMissing: false });
67
+ const db = connection.getDb();
68
+ // A replay may refer to a deactivated occurrence. Its current artifact
69
+ // bytes cannot prove what was injected at the historical event time.
70
+ // Resolve content only while the exact activation occurrence is live.
71
+ const occurrence = db.prepare(`
72
+ SELECT 1 FROM activations
73
+ WHERE activation_id = ? AND idempotency_key = ? AND deactivated_at IS NULL
74
+ `).get(activationRef.activationId, activationRef.idempotencyKey ?? '');
75
+ if (!occurrence)
76
+ return null;
77
+ const row = db.prepare('SELECT content_json, source_principle_id FROM pi_artifacts WHERE artifact_id = ?').get(activationRef.artifactId);
78
+ if (!isRecord(row)
79
+ || typeof row.content_json !== 'string'
80
+ || row.source_principle_id !== principleId)
81
+ return null;
82
+ return {
83
+ principleId,
84
+ artifactId: activationRef.artifactId,
85
+ payloadDigest: `sha256:${createHash('sha256').update(row.content_json, 'utf8').digest('hex')}`,
86
+ resolution: 'resolved',
87
+ };
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ finally {
93
+ try {
94
+ connection?.close();
95
+ }
96
+ catch { /* best-effort */ }
97
+ }
98
+ }
99
+ function appendObservationBatch(input) {
100
+ const normalization = normalizeInterventionEvidenceBatch(input.batch);
101
+ if (!normalization.ok) {
102
+ return {
103
+ ok: false,
104
+ reason: `evidence_batch_rejected:${normalization.reason}`,
105
+ nextAction: 'Fix the producer payload; nothing was written. The raw source remains durable.',
106
+ };
107
+ }
108
+ let connection = null;
109
+ try {
110
+ connection = new SqliteConnection({ workspaceDir: input.workspaceDir, busyTimeoutMs: 0 });
111
+ const store = new SqliteInterventionEvidenceStore(connection);
112
+ const expectedScope = store.getEvidenceScope()?.scopeId ?? scopeIdCandidateFor(input.workspaceDir);
113
+ if (input.batch.evidenceScopeId !== expectedScope) {
114
+ return {
115
+ ok: false,
116
+ reason: `evidence_scope_mismatch:${input.batch.evidenceScopeId}`,
117
+ nextAction: 'Resolve the evidence scope for this workspace and rebuild the batch before replay; no records were written.',
118
+ };
119
+ }
120
+ const result = store.appendObservationBatch(normalization.batch);
121
+ return {
122
+ ok: result.ok,
123
+ insertedCount: result.insertedCount,
124
+ duplicateCount: result.duplicates.length,
125
+ conflictCount: result.conflicts.length,
126
+ ...(result.reason !== undefined ? { reason: result.reason } : {}),
127
+ ...(result.nextAction !== undefined ? { nextAction: result.nextAction } : {}),
128
+ };
129
+ }
130
+ catch (error) {
131
+ const message = error instanceof Error ? error.message : String(error);
132
+ return {
133
+ ok: false,
134
+ reason: `evidence_storage_failed:${message}`,
135
+ nextAction: 'The raw source remains durable; replay the same observation batch later — idempotency makes replay safe.',
136
+ };
137
+ }
138
+ finally {
139
+ try {
140
+ connection?.close();
141
+ }
142
+ catch { /* best-effort */ }
143
+ }
144
+ }
145
+ function resolveActivationOccurrenceRef(workspaceDir, activationId) {
146
+ let connection = null;
147
+ try {
148
+ connection = new SqliteConnection({ workspaceDir, readonly: true, bootstrapIfMissing: false });
149
+ const db = connection.getDb();
150
+ const activationRows = db.prepare(`
151
+ SELECT activation_id, idempotency_key, artifact_id, channel, action, target_ref, activated_at, deactivated_at
152
+ FROM activations
153
+ WHERE activation_id = ?
154
+ ORDER BY activated_at DESC
155
+ `).all(activationId);
156
+ // Without the source's activation-time snapshot, reused ids cannot be
157
+ // matched to one historical row safely. Keep this as an explicit gap.
158
+ if (activationRows.length !== 1)
159
+ return null;
160
+ const activationRow = activationRows[0];
161
+ if (!isRecord(activationRow))
162
+ return null;
163
+ const artifactId = typeof activationRow.artifact_id === 'string' ? activationRow.artifact_id : null;
164
+ let artifactDigest = null;
165
+ // A deactivated activation row proves occurrence metadata, but the
166
+ // artifact table only exposes current content, not an as-activated copy.
167
+ if (artifactId && activationRow.deactivated_at == null) {
168
+ const artifactRow = db.prepare(`
169
+ SELECT artifact_id, artifact_kind, source_task_id, source_principle_id, source_rule_id,
170
+ lineage_artifact_ids, validation_status, content_json, created_at, updated_at
171
+ FROM pi_artifacts WHERE artifact_id = ?
172
+ `).get(artifactId);
173
+ if (isRecord(artifactRow) && typeof artifactRow.content_json === 'string') {
174
+ // Digest the governed artifact bytes exactly as captured — this is
175
+ // the content the approval/activation authority owns (SPEC §13.3).
176
+ artifactDigest = `sha256:${createHash('sha256').update(artifactRow.content_json, 'utf8').digest('hex')}`;
177
+ }
178
+ }
179
+ const snapshotBody = {
180
+ activationId,
181
+ idempotencyKey: activationRow.idempotency_key ?? null,
182
+ artifactId,
183
+ channel: activationRow.channel ?? null,
184
+ action: activationRow.action ?? null,
185
+ activatedAt: activationRow.activated_at ?? null,
186
+ artifactDigest,
187
+ };
188
+ // An activation row can survive after its artifact has been removed.
189
+ // Its metadata snapshot remains useful, but it is not a content digest.
190
+ return {
191
+ activationId,
192
+ ...(typeof activationRow.idempotency_key === 'string' ? { idempotencyKey: activationRow.idempotency_key } : {}),
193
+ ...(artifactId !== null ? { artifactId } : {}),
194
+ ...(typeof activationRow.channel === 'string' ? { channel: activationRow.channel } : {}),
195
+ ...(typeof activationRow.activated_at === 'string' ? { activatedAt: activationRow.activated_at } : {}),
196
+ sourceSnapshotDigest: `sha256:${createHash('sha256').update(JSON.stringify(snapshotBody), 'utf8').digest('hex')}`,
197
+ };
198
+ }
199
+ catch {
200
+ // Resolution is best-effort metadata for the evidence reference; an
201
+ // unreadable store leaves the caller to record the honest gap.
202
+ return null;
203
+ }
204
+ finally {
205
+ try {
206
+ connection?.close();
207
+ }
208
+ catch { /* best-effort */ }
209
+ }
210
+ }
211
+ function existingObservationRefs(workspaceDir, source) {
212
+ let connection = null;
213
+ try {
214
+ connection = new SqliteConnection({ workspaceDir, readonly: true, bootstrapIfMissing: false });
215
+ const row = connection.getDb().prepare(`
216
+ SELECT activation_ref_json, content_ref_json
217
+ FROM intervention_evidence_records WHERE source_kind = ? AND source_locator = ? AND observation_key = ?
218
+ `).get(source.sourceKind, source.sourceLocator, source.observationKey);
219
+ if (!isRecord(row) || typeof row.activation_ref_json !== 'string' || typeof row.content_ref_json !== 'string')
220
+ return null;
221
+ const activationRaw = JSON.parse(row.activation_ref_json);
222
+ const contentRaw = JSON.parse(row.content_ref_json);
223
+ const validDigest = (value) => typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value);
224
+ if (!isRecord(activationRaw) || typeof activationRaw.activationId !== 'string'
225
+ || !validDigest(activationRaw.sourceSnapshotDigest)
226
+ || !isRecord(contentRaw) || typeof contentRaw.principleId !== 'string'
227
+ || (contentRaw.resolution !== 'resolved' && contentRaw.resolution !== 'revision_reference_unresolved')
228
+ || (contentRaw.resolution === 'resolved' && !validDigest(contentRaw.payloadDigest))
229
+ || (contentRaw.resolution === 'revision_reference_unresolved' && contentRaw.payloadDigest !== undefined))
230
+ return null;
231
+ if (activationRaw.activationId !== source.activationId || contentRaw.principleId !== source.principleId
232
+ || (typeof activationRaw.artifactId === 'string' && contentRaw.artifactId !== activationRaw.artifactId)
233
+ || (source.artifactId !== undefined && (activationRaw.artifactId !== source.artifactId
234
+ || contentRaw.artifactId !== source.artifactId)))
235
+ return null;
236
+ const activationRef = {
237
+ activationId: activationRaw.activationId,
238
+ sourceSnapshotDigest: activationRaw.sourceSnapshotDigest,
239
+ ...(typeof activationRaw.idempotencyKey === 'string' ? { idempotencyKey: activationRaw.idempotencyKey } : {}),
240
+ ...(typeof activationRaw.artifactId === 'string' ? { artifactId: activationRaw.artifactId } : {}),
241
+ ...(typeof activationRaw.channel === 'string' ? { channel: activationRaw.channel } : {}),
242
+ ...(typeof activationRaw.activatedAt === 'string' ? { activatedAt: activationRaw.activatedAt } : {}),
243
+ };
244
+ const contentRef = {
245
+ principleId: contentRaw.principleId,
246
+ resolution: contentRaw.resolution,
247
+ ...(typeof contentRaw.artifactId === 'string' ? { artifactId: contentRaw.artifactId } : {}),
248
+ ...(typeof contentRaw.version === 'string' ? { version: contentRaw.version } : {}),
249
+ ...(typeof contentRaw.payloadDigest === 'string' ? { payloadDigest: contentRaw.payloadDigest } : {}),
250
+ ...(typeof contentRaw.approvalRef === 'string' ? { approvalRef: contentRaw.approvalRef } : {}),
251
+ };
252
+ return { activationRef, contentRef };
253
+ }
254
+ catch {
255
+ return null;
256
+ }
257
+ finally {
258
+ try {
259
+ connection?.close();
260
+ }
261
+ catch { /* best-effort */ }
262
+ }
263
+ }
264
+ return { appendObservationBatch, evidenceScopeIdFor, resolveInterventionContentRef, resolveActivationOccurrenceRef, existingObservationRefs };
265
+ }
266
+ /** Process-wide default ingress. */
267
+ let defaultIngress = null;
268
+ export function getInterventionEvidenceIngress() {
269
+ if (!defaultIngress)
270
+ defaultIngress = createInterventionEvidenceIngress();
271
+ return defaultIngress;
272
+ }
@@ -0,0 +1,106 @@
1
+ import { type ActivatedPrinciple } from '@principles/core/runtime-v2';
2
+ import { PROMPT_INJECTION_UNIT, type PromptInjectionBudgetScope, type PromptInjectionRouteDecision, type PromptInjectionRouteName, type PromptInjectionTargetHost } from './prompt-injection-projection.js';
3
+ /**
4
+ * SPEC PD_PROMPT_CAPACITY_V1 R-B2: ONE route-aware single-artifact capacity
5
+ * precheck, shared by every real prompt-activation write entry (Console
6
+ * approve, CLI approve) and by the pre-approval candidate fit (R-A2). The
7
+ * cost is measured with the SAME production serializer/selector the bound
8
+ * route uses — list route bills the selected-lines entry (header + entry),
9
+ * shared route bills the full single-principle directive render including
10
+ * the self-report footer when that flag is on. The principle identity is
11
+ * the REAL resolved id (content `principleId` or draft title) — never a
12
+ * fixed-length UUID allowance (a title-derived id can only be longer).
13
+ *
14
+ * This module deliberately does NOT read host configs from core: it lives
15
+ * in host-runtime and consumes the workspace config + host facts here.
16
+ */
17
+ export interface PromptDeliverabilityCandidateArtifact {
18
+ artifactId: string;
19
+ artifactKind: string;
20
+ contentJson: string;
21
+ validationStatus: string;
22
+ }
23
+ export type PromptArtifactDeliverability = {
24
+ status: 'deliverable';
25
+ route: PromptInjectionRouteName;
26
+ hostKind?: PromptInjectionTargetHost;
27
+ basis?: PromptInjectionRouteDecision['basis'];
28
+ budget: number;
29
+ /** Single-item cost in UTF-16 code units under the bound route's serializer. */
30
+ costChars: number;
31
+ budgetScope: PromptInjectionBudgetScope;
32
+ unit: typeof PROMPT_INJECTION_UNIT;
33
+ } | {
34
+ status: 'undeliverable';
35
+ reason: 'single_item_exceeds_budget';
36
+ route: PromptInjectionRouteName;
37
+ hostKind?: PromptInjectionTargetHost;
38
+ basis?: PromptInjectionRouteDecision['basis'];
39
+ budget: number;
40
+ costChars: number;
41
+ overByChars: number;
42
+ budgetScope: PromptInjectionBudgetScope;
43
+ unit: typeof PROMPT_INJECTION_UNIT;
44
+ nextAction: string;
45
+ } | {
46
+ status: 'unconfirmed';
47
+ /** Stable machine-readable reason (route facts or artifact content unresolvable). */
48
+ reason: string;
49
+ nextAction: string;
50
+ /** Route-unconfirmed case: per-host single-item fit so the UI can show both. */
51
+ perHost?: readonly {
52
+ hostKind: PromptInjectionTargetHost;
53
+ fits: boolean;
54
+ costChars: number;
55
+ }[];
56
+ };
57
+ /** Resolve the REAL principle identity + text from the artifact content. */
58
+ export declare function resolvePromptCandidatePrinciple(artifact: PromptDeliverabilityCandidateArtifact): {
59
+ ok: true;
60
+ principle: ActivatedPrinciple;
61
+ } | {
62
+ ok: false;
63
+ warning: string;
64
+ };
65
+ /**
66
+ * Positive single-item evidence under a given route, using the production
67
+ * selector itself (AC-06: a row is "fits" because its own serialized entry
68
+ * fits — never because it is absent from a bounded diagnostic list).
69
+ *
70
+ * `costChars` is what the single-entry selection would consume, measured the
71
+ * same way the route's projection reports `usedChars` (the joined selected
72
+ * lines). For an entry that does NOT fit, trimToBudget returns only the
73
+ * header line, so the would-be cost is reconstructed from the header line it
74
+ * DID return plus the real entry line (same format authority, listEntryLine).
75
+ */
76
+ export declare function computePromptSingleItemFit(input: {
77
+ principle: ActivatedPrinciple;
78
+ route: PromptInjectionRouteName;
79
+ selfReportEnabled: boolean;
80
+ }): {
81
+ fits: boolean;
82
+ costChars: number;
83
+ };
84
+ /**
85
+ * Precheck one artifact against the workspace's effective injection route.
86
+ * Pure read — no state mutation, safe to call before any governance write.
87
+ */
88
+ export declare function checkPromptArtifactDeliverability(input: {
89
+ workspaceDir: string;
90
+ targetHost?: PromptInjectionTargetHost;
91
+ artifact: PromptDeliverabilityCandidateArtifact | null | undefined;
92
+ }): Promise<PromptArtifactDeliverability>;
93
+ /** Load one artifact read-only from `<workspace>/.pd/state.db`. Failures carry a reason (rc-9). */
94
+ export declare function loadPromptArtifactById(workspaceDir: string, artifactId: string): Promise<{
95
+ ok: true;
96
+ artifact: PromptDeliverabilityCandidateArtifact;
97
+ } | {
98
+ ok: false;
99
+ reason: string;
100
+ }>;
101
+ /** Combined load + precheck for write gates that only hold an artifact id (CLI approve). */
102
+ export declare function checkPromptArtifactDeliverabilityById(input: {
103
+ workspaceDir: string;
104
+ artifactId: string;
105
+ targetHost?: PromptInjectionTargetHost;
106
+ }): Promise<PromptArtifactDeliverability>;