@eddacraft/anvil-kindling-integration 0.1.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.
Files changed (84) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +542 -0
  3. package/dist/adapter.d.ts +49 -0
  4. package/dist/adapter.d.ts.map +1 -0
  5. package/dist/adapter.js +100 -0
  6. package/dist/config.d.ts +89 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +173 -0
  9. package/dist/emitters/action-emitter.d.ts +40 -0
  10. package/dist/emitters/action-emitter.d.ts.map +1 -0
  11. package/dist/emitters/action-emitter.js +52 -0
  12. package/dist/emitters/constraint-emitter.d.ts +32 -0
  13. package/dist/emitters/constraint-emitter.d.ts.map +1 -0
  14. package/dist/emitters/constraint-emitter.js +41 -0
  15. package/dist/emitters/error-emitter.d.ts +33 -0
  16. package/dist/emitters/error-emitter.d.ts.map +1 -0
  17. package/dist/emitters/error-emitter.js +50 -0
  18. package/dist/emitters/gate-emitter.d.ts +37 -0
  19. package/dist/emitters/gate-emitter.d.ts.map +1 -0
  20. package/dist/emitters/gate-emitter.js +53 -0
  21. package/dist/emitters/human-input-emitter.d.ts +30 -0
  22. package/dist/emitters/human-input-emitter.d.ts.map +1 -0
  23. package/dist/emitters/human-input-emitter.js +38 -0
  24. package/dist/emitters/index.d.ts +13 -0
  25. package/dist/emitters/index.d.ts.map +1 -0
  26. package/dist/emitters/index.js +19 -0
  27. package/dist/emitters/plan-emitter.d.ts +75 -0
  28. package/dist/emitters/plan-emitter.d.ts.map +1 -0
  29. package/dist/emitters/plan-emitter.js +116 -0
  30. package/dist/emitters/session-emitter.d.ts +57 -0
  31. package/dist/emitters/session-emitter.d.ts.map +1 -0
  32. package/dist/emitters/session-emitter.js +80 -0
  33. package/dist/index.d.ts +40 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +111 -0
  36. package/dist/kindling-service.d.ts +122 -0
  37. package/dist/kindling-service.d.ts.map +1 -0
  38. package/dist/kindling-service.js +203 -0
  39. package/dist/observation-contract.d.ts +561 -0
  40. package/dist/observation-contract.d.ts.map +1 -0
  41. package/dist/observation-contract.js +391 -0
  42. package/dist/query-contract.d.ts +463 -0
  43. package/dist/query-contract.d.ts.map +1 -0
  44. package/dist/query-contract.js +314 -0
  45. package/dist/query-limits.d.ts +40 -0
  46. package/dist/query-limits.d.ts.map +1 -0
  47. package/dist/query-limits.js +79 -0
  48. package/dist/query-service.d.ts +109 -0
  49. package/dist/query-service.d.ts.map +1 -0
  50. package/dist/query-service.js +140 -0
  51. package/dist/retention.d.ts +79 -0
  52. package/dist/retention.d.ts.map +1 -0
  53. package/dist/retention.js +81 -0
  54. package/dist/sensitive-data-validator.d.ts +47 -0
  55. package/dist/sensitive-data-validator.d.ts.map +1 -0
  56. package/dist/sensitive-data-validator.js +135 -0
  57. package/dist/status.d.ts +104 -0
  58. package/dist/status.d.ts.map +1 -0
  59. package/dist/status.js +136 -0
  60. package/dist/utils/debug.d.ts +9 -0
  61. package/dist/utils/debug.d.ts.map +1 -0
  62. package/dist/utils/debug.js +55 -0
  63. package/package.json +114 -0
  64. package/src/adapter.ts +117 -0
  65. package/src/config.ts +202 -0
  66. package/src/emitters/action-emitter.ts +90 -0
  67. package/src/emitters/constraint-emitter.ts +73 -0
  68. package/src/emitters/error-emitter.ts +86 -0
  69. package/src/emitters/gate-emitter.ts +87 -0
  70. package/src/emitters/human-input-emitter.ts +71 -0
  71. package/src/emitters/index.ts +40 -0
  72. package/src/emitters/plan-emitter.ts +183 -0
  73. package/src/emitters/session-emitter.ts +131 -0
  74. package/src/index.ts +254 -0
  75. package/src/kindling-service.ts +272 -0
  76. package/src/malicious-ai.test.ts +949 -0
  77. package/src/observation-contract.ts +500 -0
  78. package/src/query-contract.ts +389 -0
  79. package/src/query-limits.ts +106 -0
  80. package/src/query-service.ts +217 -0
  81. package/src/retention.ts +153 -0
  82. package/src/sensitive-data-validator.ts +167 -0
  83. package/src/status.ts +221 -0
  84. package/src/utils/debug.ts +65 -0
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Kindling Query Contract (v1)
3
+ *
4
+ * Defines the read-only, bounded query surface for Kindling observations.
5
+ * This is a system of record, not a reasoning engine.
6
+ *
7
+ * GOVERNING RULE:
8
+ * Queries may retrieve facts; interpretation is the caller's responsibility.
9
+ * User-supplied AI may read, but may not mutate, infer, or generalise via Kindling.
10
+ *
11
+ * @see plans/modules/kindling-integration.aps.md for integration plan
12
+ */
13
+
14
+ import { z } from 'zod';
15
+
16
+ // =============================================================================
17
+ // Schema Version
18
+ // =============================================================================
19
+
20
+ export const KINDLING_QUERY_CONTRACT_VERSION = '1.0.0';
21
+
22
+ // =============================================================================
23
+ // Query Scopes (Mandatory Boundary)
24
+ // =============================================================================
25
+
26
+ /**
27
+ * Every query must specify exactly one scope.
28
+ * No free-text search. No global scans. No cross-project reads.
29
+ */
30
+ export const QueryScopeSchema = z.enum([
31
+ 'session', // "What happened in this run?"
32
+ 'plan', // "What happened because of this plan?"
33
+ 'gate', // "Why did this gate pass/fail?"
34
+ 'action', // "What exactly did this action do?"
35
+ ]);
36
+
37
+ export type QueryScope = z.infer<typeof QueryScopeSchema>;
38
+
39
+ // =============================================================================
40
+ // Result Shape
41
+ // =============================================================================
42
+
43
+ /**
44
+ * How results should be structured
45
+ */
46
+ export const ResultShapeSchema = z.enum([
47
+ 'timeline', // Ordered observations grouped by phase
48
+ 'list', // Flat list of observations
49
+ 'entity', // Single entity with metadata
50
+ ]);
51
+
52
+ export type ResultShape = z.infer<typeof ResultShapeSchema>;
53
+
54
+ // =============================================================================
55
+ // Output Format
56
+ // =============================================================================
57
+
58
+ /**
59
+ * Result serialisation format
60
+ */
61
+ export const OutputFormatSchema = z.enum([
62
+ 'json', // Machine-readable
63
+ 'text', // Human-readable (for CLI)
64
+ ]);
65
+
66
+ export type OutputFormat = z.infer<typeof OutputFormatSchema>;
67
+
68
+ // =============================================================================
69
+ // Query Request (Base)
70
+ // =============================================================================
71
+
72
+ /**
73
+ * Base query request with mandatory constraints
74
+ */
75
+ export const QueryRequestBaseSchema = z.object({
76
+ scope: QueryScopeSchema.describe('Query scope (mandatory)'),
77
+ shape: ResultShapeSchema.describe('Result structure (mandatory)'),
78
+ format: OutputFormatSchema.default('json').describe('Output format'),
79
+
80
+ // Time bounds (optional but encouraged)
81
+ time_after: z.string().datetime().optional().describe('Include observations after this time'),
82
+ time_before: z.string().datetime().optional().describe('Include observations before this time'),
83
+
84
+ // Result limits (anti-vacuum-cleaner)
85
+ max_results: z
86
+ .number()
87
+ .int()
88
+ .positive()
89
+ .max(1000)
90
+ .default(100)
91
+ .describe('Maximum observations to return'),
92
+ max_payload_bytes: z
93
+ .number()
94
+ .int()
95
+ .positive()
96
+ .max(10 * 1024 * 1024) // 10MB
97
+ .default(1024 * 1024) // 1MB
98
+ .describe('Maximum total payload size'),
99
+ });
100
+
101
+ export type QueryRequestBase = z.infer<typeof QueryRequestBaseSchema>;
102
+
103
+ // =============================================================================
104
+ // Session Query
105
+ // =============================================================================
106
+
107
+ /**
108
+ * Query: "What happened in this run?"
109
+ *
110
+ * Returns ordered observations grouped by phase (plan / gate / action / outcome).
111
+ * Raw payloads only, no summaries.
112
+ */
113
+ export const SessionQuerySchema = QueryRequestBaseSchema.extend({
114
+ scope: z.literal('session'),
115
+ session_id: z.string().uuid().describe('Session/run ID (mandatory)'),
116
+ include_phases: z
117
+ .array(z.enum(['plan', 'gate', 'action', 'outcome', 'error']))
118
+ .optional()
119
+ .describe('Filter to specific phases'),
120
+ });
121
+
122
+ export type SessionQuery = z.infer<typeof SessionQuerySchema>;
123
+
124
+ // =============================================================================
125
+ // Plan Query
126
+ // =============================================================================
127
+
128
+ /**
129
+ * Query: "What happened because of this plan?"
130
+ *
131
+ * Returns plan metadata, versions, and linked executions.
132
+ * This is the ONLY cross-session read allowed, via explicit plan_id.
133
+ */
134
+ export const PlanQuerySchema = QueryRequestBaseSchema.extend({
135
+ scope: z.literal('plan'),
136
+ plan_id: z.string().describe('Plan ID (mandatory)'),
137
+ include_executions: z.boolean().default(true).describe('Include linked execution run IDs'),
138
+ include_versions: z.boolean().default(true).describe('Include plan version history'),
139
+ });
140
+
141
+ export type PlanQuery = z.infer<typeof PlanQuerySchema>;
142
+
143
+ // =============================================================================
144
+ // Gate Query
145
+ // =============================================================================
146
+
147
+ /**
148
+ * Query: "Why did this gate pass/fail?"
149
+ *
150
+ * Returns gate evaluation details with rule IDs, inputs (sanitised), and outcomes.
151
+ * No prose. No explanation layer.
152
+ */
153
+ export const GateQuerySchema = QueryRequestBaseSchema.extend({
154
+ scope: z.literal('gate'),
155
+ gate_eval_id: z.string().describe('Gate evaluation ID (mandatory)'),
156
+ });
157
+
158
+ export type GateQuery = z.infer<typeof GateQuerySchema>;
159
+
160
+ // =============================================================================
161
+ // Action Query
162
+ // =============================================================================
163
+
164
+ /**
165
+ * Query: "What exactly did this action do?"
166
+ *
167
+ * Returns action details with redacted command, environment, and linked governance.
168
+ * This is the atomic unit of accountability.
169
+ */
170
+ export const ActionQuerySchema = QueryRequestBaseSchema.extend({
171
+ scope: z.literal('action'),
172
+ action_id: z.string().describe('Action ID (mandatory)'),
173
+ include_approval_chain: z
174
+ .boolean()
175
+ .default(true)
176
+ .describe('Include approval requirements and state'),
177
+ });
178
+
179
+ export type ActionQuery = z.infer<typeof ActionQuerySchema>;
180
+
181
+ // =============================================================================
182
+ // Query Request (Union)
183
+ // =============================================================================
184
+
185
+ /**
186
+ * All query types (discriminated union)
187
+ */
188
+ export const QueryRequestSchema = z.discriminatedUnion('scope', [
189
+ SessionQuerySchema,
190
+ PlanQuerySchema,
191
+ GateQuerySchema,
192
+ ActionQuerySchema,
193
+ ]);
194
+
195
+ export type QueryRequest = z.infer<typeof QueryRequestSchema>;
196
+
197
+ // =============================================================================
198
+ // Query Response (Output Guarantees)
199
+ // =============================================================================
200
+
201
+ /**
202
+ * Standard metadata for all responses
203
+ */
204
+ export const QueryResponseMetadataSchema = z.object({
205
+ query_id: z.string().uuid().describe('Unique query identifier (for debugging)'),
206
+ executed_at: z.string().datetime().describe('When query was executed'),
207
+ contract_version: z.string().describe('Query contract version used'),
208
+ result_count: z.number().int().nonnegative().describe('Number of observations returned'),
209
+ truncated: z.boolean().describe('Whether results were truncated (hit limits)'),
210
+ truncation_reason: z
211
+ .enum(['max_results', 'max_payload_bytes', 'none'])
212
+ .optional()
213
+ .describe('Why truncation occurred'),
214
+ });
215
+
216
+ export type QueryResponseMetadata = z.infer<typeof QueryResponseMetadataSchema>;
217
+
218
+ /**
219
+ * Standard provenance link
220
+ */
221
+ export const ProvenanceLinkSchema = z.object({
222
+ type: z.enum(['caused_by', 'governed_by', 'approved_by', 'linked_to']).describe('Link type'),
223
+ entity_type: z
224
+ .enum(['session', 'plan', 'gate', 'action', 'human'])
225
+ .describe('Target entity type'),
226
+ entity_id: z.string().describe('Target entity ID'),
227
+ timestamp: z.string().datetime().describe('When link was created'),
228
+ });
229
+
230
+ export type ProvenanceLink = z.infer<typeof ProvenanceLinkSchema>;
231
+
232
+ /**
233
+ * Observation (base type for all returned data)
234
+ */
235
+ export const ObservationSchema = z.object({
236
+ id: z.string().uuid().describe('Observation ID'),
237
+ kind: z
238
+ .enum([
239
+ 'session_start',
240
+ 'session_end',
241
+ 'plan_created',
242
+ 'plan_edited',
243
+ 'plan_approved',
244
+ 'plan_rejected',
245
+ 'gate_evaluated',
246
+ 'action_executed',
247
+ 'constraint_applied',
248
+ 'human_input',
249
+ 'error',
250
+ ])
251
+ .describe('Observation kind'),
252
+ timestamp: z.string().datetime().describe('When observation was recorded'),
253
+ session_id: z.string().uuid().describe('Session this observation belongs to'),
254
+
255
+ // Provenance (explicit links)
256
+ provenance: z.array(ProvenanceLinkSchema).describe('Explicit links to other entities'),
257
+
258
+ // Payload (fact data, no inference)
259
+ payload: z.record(z.string(), z.unknown()).describe('Observation-specific data (raw facts only)'),
260
+ });
261
+
262
+ export type Observation = z.infer<typeof ObservationSchema>;
263
+
264
+ /**
265
+ * Query response
266
+ */
267
+ export const QueryResponseSchema = z.object({
268
+ metadata: QueryResponseMetadataSchema.describe('Query execution metadata'),
269
+ observations: z.array(ObservationSchema).describe('Ordered observations (facts only)'),
270
+ });
271
+
272
+ export type QueryResponse = z.infer<typeof QueryResponseSchema>;
273
+
274
+ // =============================================================================
275
+ // Output Guarantees (Documented Requirements)
276
+ // =============================================================================
277
+
278
+ /**
279
+ * Every Kindling response guarantees:
280
+ *
281
+ * 1. Stable field names - No field names change between queries
282
+ * 2. Explicit timestamps - Every observation has ISO8601 timestamp
283
+ * 3. Explicit links - Provenance via typed links (caused_by, governed_by, approved_by)
284
+ * 4. No hidden inference - Payload contains only raw facts
285
+ * 5. No reordered history - Observations returned in recorded order
286
+ *
287
+ * This makes Kindling LLM-safe by construction.
288
+ *
289
+ * AI can:
290
+ * - Narrate events
291
+ * - Summarise outcomes
292
+ * - Explain facts
293
+ *
294
+ * But AI will always be explaining facts, not ghosts.
295
+ */
296
+
297
+ // =============================================================================
298
+ // Read-Only Enforcement (Anti-Pattern Markers)
299
+ // =============================================================================
300
+
301
+ /**
302
+ * Operations that MUST NOT exist in the query API:
303
+ *
304
+ * ❌ write()
305
+ * ❌ update()
306
+ * ❌ delete()
307
+ * ❌ annotate()
308
+ * ❌ tag()
309
+ * ❌ learn()
310
+ * ❌ embed()
311
+ * ❌ infer()
312
+ *
313
+ * If user AI wants memory, it must bring its own store.
314
+ */
315
+
316
+ // =============================================================================
317
+ // Explicit Non-Goals (v1 Boundary)
318
+ // =============================================================================
319
+
320
+ /**
321
+ * The following are explicitly OUT OF SCOPE for v1:
322
+ *
323
+ * ❌ Semantic search
324
+ * ❌ Similarity queries
325
+ * ❌ Embeddings
326
+ * ❌ Cross-plan discovery
327
+ * ❌ Learned relevance
328
+ * ❌ Auto-summaries (stored in Kindling)
329
+ * ❌ AI-generated annotations stored in Kindling
330
+ *
331
+ * These belong to Edda / Ember, not Kindling v1.
332
+ */
333
+
334
+ // =============================================================================
335
+ // Validation Utilities
336
+ // =============================================================================
337
+
338
+ /**
339
+ * Validate a query request
340
+ */
341
+ export function validateQueryRequest(data: unknown): {
342
+ success: boolean;
343
+ data?: QueryRequest;
344
+ error?: string;
345
+ } {
346
+ const result = QueryRequestSchema.safeParse(data);
347
+ if (result.success) {
348
+ return { success: true, data: result.data };
349
+ }
350
+ return { success: false, error: result.error.format()._errors.join(', ') };
351
+ }
352
+
353
+ /**
354
+ * Validate a query response
355
+ */
356
+ export function validateQueryResponse(data: unknown): {
357
+ success: boolean;
358
+ data?: QueryResponse;
359
+ error?: string;
360
+ } {
361
+ const result = QueryResponseSchema.safeParse(data);
362
+ if (result.success) {
363
+ return { success: true, data: result.data };
364
+ }
365
+ return { success: false, error: result.error.format()._errors.join(', ') };
366
+ }
367
+
368
+ // =============================================================================
369
+ // CLI Command Mapping (Human-First, AI-Compatible)
370
+ // =============================================================================
371
+
372
+ /**
373
+ * All queries have CLI equivalents:
374
+ *
375
+ * anvil run show <run_id> --json
376
+ * → SessionQuery { scope: 'session', session_id: run_id, shape: 'timeline' }
377
+ *
378
+ * anvil plan trace <plan_id> --json
379
+ * → PlanQuery { scope: 'plan', plan_id: plan_id, shape: 'entity' }
380
+ *
381
+ * anvil gate show <gate_eval_id> --json
382
+ * → GateQuery { scope: 'gate', gate_eval_id: gate_eval_id, shape: 'entity' }
383
+ *
384
+ * anvil action show <action_id> --json
385
+ * → ActionQuery { scope: 'action', action_id: action_id, shape: 'entity' }
386
+ *
387
+ * The CLI is a thin wrapper over this query surface.
388
+ * That symmetry is intentional.
389
+ */
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Query Limits Enforcement (KINDLING-010)
3
+ *
4
+ * Provides post-query truncation and metadata flagging.
5
+ * Used by KindlingQueryService before returning results to callers,
6
+ * as a defense-in-depth layer on top of store-level limits.
7
+ */
8
+
9
+ import type { QueryResponse, QueryResponseMetadata } from './query-contract.js';
10
+ import type { QueryLimitConfig } from './config.js';
11
+
12
+ // =============================================================================
13
+ // Types
14
+ // =============================================================================
15
+
16
+ /**
17
+ * Limits to enforce on a query response
18
+ */
19
+ export interface QueryLimits {
20
+ /** Maximum number of observations to return */
21
+ max_results: number;
22
+ /** Maximum total payload size in bytes */
23
+ max_payload_bytes: number;
24
+ }
25
+
26
+ // =============================================================================
27
+ // Enforcement
28
+ // =============================================================================
29
+
30
+ /**
31
+ * Enforce query limits on a response by truncating results if necessary.
32
+ *
33
+ * This is applied after the store returns results, as a safety net.
34
+ * If the store already respects limits, this is a no-op.
35
+ *
36
+ * Sets the `truncated` and `truncation_reason` metadata flags when
37
+ * results are truncated.
38
+ *
39
+ * @param response - The query response from the store
40
+ * @param limits - The limits to enforce
41
+ * @returns A new QueryResponse with limits enforced
42
+ */
43
+ export function enforceQueryLimits(response: QueryResponse, limits: QueryLimits): QueryResponse {
44
+ let observations = response.observations;
45
+ let truncated = response.metadata.truncated;
46
+ let truncationReason = response.metadata.truncation_reason;
47
+
48
+ // Check max_results
49
+ if (observations.length > limits.max_results) {
50
+ observations = observations.slice(0, limits.max_results);
51
+ truncated = true;
52
+ truncationReason = 'max_results';
53
+ }
54
+
55
+ // Check max_payload_bytes
56
+ const serialized = JSON.stringify(observations);
57
+ const payloadBytes = new TextEncoder().encode(serialized).byteLength;
58
+
59
+ if (payloadBytes > limits.max_payload_bytes) {
60
+ // Binary search for the maximum number of observations that fit
61
+ let lo = 0;
62
+ let hi = observations.length;
63
+
64
+ while (lo < hi) {
65
+ const mid = Math.floor((lo + hi + 1) / 2);
66
+ const slice = observations.slice(0, mid);
67
+ const sliceBytes = new TextEncoder().encode(JSON.stringify(slice)).byteLength;
68
+
69
+ if (sliceBytes <= limits.max_payload_bytes) {
70
+ lo = mid;
71
+ } else {
72
+ hi = mid - 1;
73
+ }
74
+ }
75
+
76
+ observations = observations.slice(0, lo);
77
+ truncated = true;
78
+ // Only override reason if not already set (max_results takes precedence)
79
+ truncationReason = truncationReason === 'max_results' ? 'max_results' : 'max_payload_bytes';
80
+ }
81
+
82
+ const metadata: QueryResponseMetadata = {
83
+ ...response.metadata,
84
+ result_count: observations.length,
85
+ truncated,
86
+ truncation_reason: truncated ? truncationReason : 'none',
87
+ };
88
+
89
+ return {
90
+ metadata,
91
+ observations,
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Create QueryLimits from a QueryLimitConfig.
97
+ *
98
+ * @param config - Query limit configuration
99
+ * @returns QueryLimits object
100
+ */
101
+ export function limitsFromConfig(config: QueryLimitConfig): QueryLimits {
102
+ return {
103
+ max_results: config.max_results,
104
+ max_payload_bytes: config.max_payload_bytes,
105
+ };
106
+ }