@gmickel/gno 1.16.0 → 1.17.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 (46) hide show
  1. package/README.md +20 -16
  2. package/assets/skill/SKILL.md +8 -5
  3. package/package.json +1 -1
  4. package/src/app/context-agent-projection.ts +303 -0
  5. package/src/app/context-format.ts +249 -0
  6. package/src/app/context-runtime-contract.ts +325 -0
  7. package/src/app/context-runtime-input.ts +362 -0
  8. package/src/app/context-runtime-types.ts +65 -0
  9. package/src/app/context-runtime.ts +170 -0
  10. package/src/app/context-surface.ts +145 -0
  11. package/src/cli/commands/context-build.ts +149 -0
  12. package/src/cli/commands/context-verify.ts +90 -0
  13. package/src/cli/options.ts +4 -0
  14. package/src/cli/program.ts +178 -0
  15. package/src/core/context-budget.ts +461 -0
  16. package/src/core/context-capsule-index-schema.ts +15 -0
  17. package/src/core/context-capsule-retrieval-schema.ts +81 -0
  18. package/src/core/context-capsule-schema.ts +473 -0
  19. package/src/core/context-capsule-validation.ts +416 -0
  20. package/src/core/context-capsule-verification.ts +218 -0
  21. package/src/core/context-capsule.ts +439 -0
  22. package/src/core/context-compiler.ts +513 -0
  23. package/src/core/context-evidence-metadata.ts +33 -0
  24. package/src/core/context-evidence.ts +495 -0
  25. package/src/core/context-facets.ts +163 -0
  26. package/src/core/context-guidance.ts +69 -0
  27. package/src/core/context-scope.ts +32 -0
  28. package/src/core/context-verifier-canonical.ts +90 -0
  29. package/src/core/context-verifier-input.ts +66 -0
  30. package/src/core/context-verifier.ts +447 -0
  31. package/src/core/sections.ts +63 -0
  32. package/src/mcp/server.ts +10 -4
  33. package/src/mcp/tools/context.ts +229 -0
  34. package/src/mcp/tools/index.ts +27 -0
  35. package/src/pipeline/chunk-lookup.ts +33 -0
  36. package/src/pipeline/hybrid.ts +79 -57
  37. package/src/pipeline/types.ts +14 -0
  38. package/src/sdk/client.ts +68 -6
  39. package/src/sdk/index.ts +21 -0
  40. package/src/sdk/types.ts +24 -0
  41. package/src/serve/background-runtime.ts +1 -0
  42. package/src/serve/context-capsule.ts +136 -0
  43. package/src/serve/context.ts +10 -1
  44. package/src/serve/routes/api.ts +2 -0
  45. package/src/serve/server.ts +23 -0
  46. package/src/store/sqlite/adapter.ts +38 -20
@@ -0,0 +1,439 @@
1
+ import { z } from "zod";
2
+
3
+ import { canonicalizeIndexName } from "../app/index-name";
4
+ import {
5
+ contextCapsulePayloadV1Schema,
6
+ type ContextCapsulePayloadV1,
7
+ } from "./context-capsule-schema";
8
+
9
+ const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/);
10
+
11
+ const compareCodeUnits = (left: string, right: string): number => {
12
+ if (left < right) return -1;
13
+ if (left > right) return 1;
14
+ return 0;
15
+ };
16
+
17
+ const canonicalizeJsonValue = (value: unknown): unknown => {
18
+ if (Array.isArray(value)) return value.map(canonicalizeJsonValue);
19
+ if (value !== null && typeof value === "object") {
20
+ const sorted: Record<string, unknown> = {};
21
+ for (const key of Object.keys(value).sort(compareCodeUnits)) {
22
+ const child = (value as Record<string, unknown>)[key];
23
+ if (child === undefined) {
24
+ throw new Error(`Canonical JSON rejects undefined at ${key}`);
25
+ }
26
+ sorted[key] = canonicalizeJsonValue(child);
27
+ }
28
+ return sorted;
29
+ }
30
+ if (typeof value === "number" && !Number.isFinite(value)) {
31
+ throw new Error("Canonical JSON rejects non-finite numbers");
32
+ }
33
+ return value;
34
+ };
35
+
36
+ const canonicalJson = (value: unknown): string =>
37
+ JSON.stringify(canonicalizeJsonValue(value));
38
+ const utf8Bytes = (value: string): number =>
39
+ new TextEncoder().encode(value).byteLength;
40
+ const hashCanonical = (value: unknown): string =>
41
+ new Bun.CryptoHasher("sha256").update(canonicalJson(value)).digest("hex");
42
+ const normalizeText = (value: string): string =>
43
+ value.replace(/\r\n/g, "\n").replace(/\r/g, "\n").normalize("NFC");
44
+ const normalizeDate = (value: string | null): string | null =>
45
+ value === null ? null : new Date(value).toISOString();
46
+ const normalizeDocumentDate = (value: string | null): string | null =>
47
+ value === null || /^\d{4}-\d{2}-\d{2}$/.test(value)
48
+ ? value
49
+ : new Date(value).toISOString();
50
+ const normalizeSet = (values: readonly string[]): string[] =>
51
+ [...new Set(values.map(normalizeText))].sort(compareCodeUnits);
52
+
53
+ const normalizePayload = (
54
+ value: ContextCapsulePayloadV1
55
+ ): ContextCapsulePayloadV1 => ({
56
+ ...value,
57
+ goal: normalizeText(value.goal),
58
+ query: normalizeText(value.query),
59
+ scope: {
60
+ ...value.scope,
61
+ indexName: canonicalizeIndexName(value.scope.indexName),
62
+ collections: normalizeSet(value.scope.collections),
63
+ uriPrefix:
64
+ value.scope.uriPrefix === null
65
+ ? null
66
+ : normalizeText(value.scope.uriPrefix),
67
+ tagsAll: normalizeSet(value.scope.tagsAll),
68
+ tagsAny: normalizeSet(value.scope.tagsAny),
69
+ categories: normalizeSet(value.scope.categories),
70
+ since: normalizeDate(value.scope.since),
71
+ until: normalizeDate(value.scope.until),
72
+ },
73
+ retrieval: {
74
+ ...value.retrieval,
75
+ facets: normalizeSet(value.retrieval.facets),
76
+ queryVariants: value.retrieval.queryVariants.map(normalizeText),
77
+ request: {
78
+ ...value.retrieval.request,
79
+ author:
80
+ value.retrieval.request.author === null
81
+ ? null
82
+ : normalizeText(value.retrieval.request.author),
83
+ lang:
84
+ value.retrieval.request.lang === null
85
+ ? null
86
+ : normalizeText(value.retrieval.request.lang),
87
+ queryModes: value.retrieval.request.queryModes.map((mode) => ({
88
+ ...mode,
89
+ text: normalizeText(mode.text),
90
+ })),
91
+ },
92
+ capabilityStates: Object.fromEntries(
93
+ Object.entries(value.retrieval.capabilityStates).map(([key, state]) => [
94
+ key,
95
+ { ...state, fallbackReasons: normalizeSet(state.fallbackReasons) },
96
+ ])
97
+ ) as typeof value.retrieval.capabilityStates,
98
+ },
99
+ fallbacks: [...value.fallbacks].sort((left, right) =>
100
+ compareCodeUnits(
101
+ `${left.code}\0${left.capability}`,
102
+ `${right.code}\0${right.capability}`
103
+ )
104
+ ),
105
+ evidence: value.evidence.map((item) => ({
106
+ ...item,
107
+ title: item.title === null ? null : normalizeText(item.title),
108
+ heading: item.heading === null ? null : normalizeText(item.heading),
109
+ modifiedAt: normalizeDate(item.modifiedAt),
110
+ documentDate: normalizeDocumentDate(item.documentDate),
111
+ observedAt: normalizeDate(item.observedAt),
112
+ contextIds: normalizeSet(item.contextIds),
113
+ facets: normalizeSet(item.facets),
114
+ })),
115
+ guidance: {
116
+ ...value.guidance,
117
+ configuredContexts: [...value.guidance.configuredContexts]
118
+ .map((item) => ({
119
+ ...item,
120
+ scopeKey: normalizeText(item.scopeKey),
121
+ text: normalizeText(item.text),
122
+ }))
123
+ .sort((left, right) => compareCodeUnits(left.contextId, right.contextId)),
124
+ },
125
+ coverage: {
126
+ ...value.coverage,
127
+ requestedFacets: normalizeSet(value.coverage.requestedFacets),
128
+ coveredFacets: [...value.coverage.coveredFacets]
129
+ .map((item) => ({
130
+ facet: normalizeText(item.facet),
131
+ evidenceIds: normalizeSet(item.evidenceIds),
132
+ }))
133
+ .sort((left, right) => compareCodeUnits(left.facet, right.facet)),
134
+ unresolvedFacets: normalizeSet(value.coverage.unresolvedFacets),
135
+ gaps: [...value.coverage.gaps].sort((left, right) =>
136
+ compareCodeUnits(
137
+ `${left.facet}\0${left.code}`,
138
+ `${right.facet}\0${right.code}`
139
+ )
140
+ ),
141
+ },
142
+ warnings: [...value.warnings].sort((left, right) =>
143
+ compareCodeUnits(left.code, right.code)
144
+ ),
145
+ });
146
+
147
+ export type ContextCapsuleErrorCode =
148
+ | "identity_mismatch"
149
+ | "index_changed_during_compile"
150
+ | "invalid_budget"
151
+ | "invalid_input"
152
+ | "no_evidence"
153
+ | "tokenizer_unavailable";
154
+
155
+ export class ContextCapsuleContractError extends Error {
156
+ readonly code: ContextCapsuleErrorCode;
157
+
158
+ constructor(
159
+ code: ContextCapsuleErrorCode,
160
+ message: string,
161
+ options?: ErrorOptions
162
+ ) {
163
+ super(message, options);
164
+ this.name = "ContextCapsuleContractError";
165
+ this.code = code;
166
+ }
167
+ }
168
+
169
+ const contractError = (
170
+ input: unknown,
171
+ error: unknown
172
+ ): ContextCapsuleContractError => {
173
+ const record =
174
+ input && typeof input === "object"
175
+ ? (input as Record<string, unknown>)
176
+ : {};
177
+ const evidence = record.evidence;
178
+ const retrieval = record.retrieval as Record<string, unknown> | undefined;
179
+ const snapshot = retrieval?.indexSnapshot as
180
+ | Record<string, unknown>
181
+ | undefined;
182
+ if (Array.isArray(evidence) && evidence.length === 0) {
183
+ return new ContextCapsuleContractError(
184
+ "no_evidence",
185
+ "Context Capsule requires evidence",
186
+ {
187
+ cause: error,
188
+ }
189
+ );
190
+ }
191
+ if (snapshot?.before !== snapshot?.after || snapshot?.stable === false) {
192
+ return new ContextCapsuleContractError(
193
+ "index_changed_during_compile",
194
+ "Index changed while Context Capsule compilation was running",
195
+ { cause: error }
196
+ );
197
+ }
198
+ if (record.budget !== undefined) {
199
+ const budget = record.budget as Record<string, unknown>;
200
+ if (
201
+ typeof budget.usedBytes !== "number" ||
202
+ typeof budget.requestedBytes !== "number" ||
203
+ typeof budget.safetyMarginBytes !== "number" ||
204
+ typeof budget.usedTokens !== "number" ||
205
+ typeof budget.requestedTokens !== "number" ||
206
+ typeof budget.safetyMarginTokens !== "number" ||
207
+ budget.safetyMarginBytes < 0 ||
208
+ budget.safetyMarginTokens < 0 ||
209
+ budget.usedBytes > budget.requestedBytes ||
210
+ budget.usedTokens > budget.requestedTokens ||
211
+ budget.usedBytes + budget.safetyMarginBytes > budget.requestedBytes ||
212
+ budget.usedTokens + budget.safetyMarginTokens > budget.requestedTokens
213
+ ) {
214
+ return new ContextCapsuleContractError(
215
+ "invalid_budget",
216
+ "Invalid Context Capsule budget",
217
+ {
218
+ cause: error,
219
+ }
220
+ );
221
+ }
222
+ }
223
+ return new ContextCapsuleContractError(
224
+ "invalid_input",
225
+ "Invalid Context Capsule payload",
226
+ {
227
+ cause: error,
228
+ }
229
+ );
230
+ };
231
+
232
+ const parsePayload = (input: unknown): ContextCapsulePayloadV1 => {
233
+ try {
234
+ return contextCapsulePayloadV1Schema.parse(input);
235
+ } catch (error) {
236
+ throw contractError(input, error);
237
+ }
238
+ };
239
+
240
+ export interface ContextCapsuleCreateOptions {
241
+ countTokens?: (accountingJson: string) => number;
242
+ }
243
+
244
+ const identityPayload = (payload: ContextCapsulePayloadV1) => {
245
+ const {
246
+ usedBytes: _usedBytes,
247
+ usedTokens: _usedTokens,
248
+ ...stableBudget
249
+ } = payload.budget;
250
+ return { ...payload, budget: stableBudget };
251
+ };
252
+
253
+ const accountingPayload = <T extends ContextCapsulePayloadV1>(value: T) => {
254
+ const {
255
+ usedBytes: _usedBytes,
256
+ usedTokens: _usedTokens,
257
+ ...budget
258
+ } = value.budget;
259
+ return { ...value, budget };
260
+ };
261
+
262
+ const activeTokenCount = (
263
+ accountingJson: string,
264
+ options: ContextCapsuleCreateOptions
265
+ ): number => {
266
+ const count = options.countTokens?.(accountingJson);
267
+ if (count === undefined || !Number.isSafeInteger(count) || count < 1) {
268
+ throw new ContextCapsuleContractError(
269
+ "invalid_budget",
270
+ "Active-tokenizer Capsules require a positive deterministic token counter"
271
+ );
272
+ }
273
+ return count;
274
+ };
275
+
276
+ const fixedPointCapsule = (
277
+ payloadInput: ContextCapsulePayloadV1,
278
+ options: ContextCapsuleCreateOptions
279
+ ) => {
280
+ const capsuleId = hashCanonical(identityPayload(payloadInput));
281
+ const stableCapsule = { ...payloadInput, capsuleId };
282
+ const exactTokens =
283
+ payloadInput.budget.estimator === "active_tokenizer"
284
+ ? activeTokenCount(
285
+ canonicalJson(accountingPayload(stableCapsule)),
286
+ options
287
+ )
288
+ : null;
289
+ let payload = {
290
+ ...payloadInput,
291
+ budget: {
292
+ ...payloadInput.budget,
293
+ usedBytes: 0,
294
+ usedTokens: exactTokens ?? 1,
295
+ },
296
+ };
297
+ const visited = new Set<string>();
298
+ for (let iteration = 0; iteration < 32; iteration += 1) {
299
+ const capsule = { ...payload, capsuleId };
300
+ const canonical = canonicalJson(capsule);
301
+ const measuredBytes = utf8Bytes(canonical);
302
+ const tokenCount = exactTokens ?? measuredBytes;
303
+ if (
304
+ measuredBytes === payload.budget.usedBytes &&
305
+ tokenCount === payload.budget.usedTokens
306
+ ) {
307
+ return capsule;
308
+ }
309
+ const state = `${measuredBytes}:${tokenCount}`;
310
+ if (visited.has(state)) break;
311
+ visited.add(state);
312
+ payload = {
313
+ ...payload,
314
+ budget: {
315
+ ...payload.budget,
316
+ usedBytes: measuredBytes,
317
+ usedTokens: tokenCount,
318
+ },
319
+ };
320
+ }
321
+ throw new ContextCapsuleContractError(
322
+ "invalid_budget",
323
+ "Canonical Context Capsule budget did not converge"
324
+ );
325
+ };
326
+
327
+ const contextCapsuleBaseV1Schema = contextCapsulePayloadV1Schema.extend({
328
+ capsuleId: sha256Schema,
329
+ });
330
+
331
+ export const contextCapsuleV1Schema = contextCapsuleBaseV1Schema.superRefine(
332
+ (value, context) => {
333
+ const { capsuleId, ...payload } = value;
334
+ if (hashCanonical(identityPayload(payload)) !== capsuleId) {
335
+ context.addIssue({
336
+ code: "custom",
337
+ message: "capsuleId does not match payload",
338
+ path: ["capsuleId"],
339
+ });
340
+ }
341
+ if (utf8Bytes(canonicalJson(value)) !== value.budget.usedBytes) {
342
+ context.addIssue({
343
+ code: "custom",
344
+ message: "usedBytes must equal canonical Capsule JSON UTF-8 bytes",
345
+ path: ["budget", "usedBytes"],
346
+ });
347
+ }
348
+ if (
349
+ value.budget.estimator === "unicode_conservative" &&
350
+ value.budget.usedTokens !== value.budget.usedBytes
351
+ ) {
352
+ context.addIssue({
353
+ code: "custom",
354
+ message: "unicode-conservative usedTokens must equal final usedBytes",
355
+ path: ["budget", "usedTokens"],
356
+ });
357
+ }
358
+ }
359
+ );
360
+
361
+ export type ContextCapsuleV1 = z.infer<typeof contextCapsuleV1Schema>;
362
+
363
+ export const contextCapsuleId = (
364
+ input: unknown,
365
+ options: ContextCapsuleCreateOptions = {}
366
+ ): string => createContextCapsuleV1(input, options).capsuleId;
367
+
368
+ export const createContextCapsuleV1 = (
369
+ input: unknown,
370
+ options: ContextCapsuleCreateOptions = {}
371
+ ): ContextCapsuleV1 => {
372
+ const parsed = normalizePayload(parsePayload(input));
373
+ const capsule = fixedPointCapsule(parsed, options);
374
+ if (
375
+ capsule.budget.usedBytes + capsule.budget.safetyMarginBytes >
376
+ capsule.budget.requestedBytes ||
377
+ capsule.budget.usedTokens + capsule.budget.safetyMarginTokens >
378
+ capsule.budget.requestedTokens
379
+ ) {
380
+ throw new ContextCapsuleContractError(
381
+ "invalid_budget",
382
+ "Canonical Context Capsule JSON exceeds its global budget"
383
+ );
384
+ }
385
+ return contextCapsuleV1Schema.parse(capsule);
386
+ };
387
+
388
+ export const parseContextCapsuleV1 = (
389
+ input: unknown,
390
+ options: ContextCapsuleCreateOptions = {}
391
+ ): ContextCapsuleV1 => {
392
+ try {
393
+ const parsed = contextCapsuleBaseV1Schema.parse(input);
394
+ const { capsuleId, ...payloadInput } = parsed;
395
+ const payload = normalizePayload(payloadInput);
396
+ const canonical = { ...payload, capsuleId };
397
+ const result = contextCapsuleV1Schema.safeParse(canonical);
398
+ if (!result.success) {
399
+ throw new ContextCapsuleContractError(
400
+ result.error.issues.some((issue) => issue.path.includes("capsuleId"))
401
+ ? "identity_mismatch"
402
+ : "invalid_budget",
403
+ "Context Capsule canonical identity or budget does not match its payload",
404
+ { cause: result.error }
405
+ );
406
+ }
407
+ if (
408
+ result.data.budget.estimator === "active_tokenizer" &&
409
+ options.countTokens !== undefined
410
+ ) {
411
+ const recounted = activeTokenCount(
412
+ canonicalJson(accountingPayload(result.data)),
413
+ options
414
+ );
415
+ if (recounted !== result.data.budget.usedTokens) {
416
+ throw new ContextCapsuleContractError(
417
+ "invalid_budget",
418
+ "usedTokens does not match the active-tokenizer accounting projection"
419
+ );
420
+ }
421
+ }
422
+ return result.data;
423
+ } catch (error) {
424
+ if (error instanceof ContextCapsuleContractError) throw error;
425
+ throw contractError(input, error);
426
+ }
427
+ };
428
+
429
+ export const canonicalContextCapsuleJson = (input: unknown): string =>
430
+ canonicalJson(parseContextCapsuleV1(input));
431
+
432
+ export const canonicalContextCapsuleAccountingJson = (input: unknown): string =>
433
+ canonicalJson(accountingPayload(parseContextCapsuleV1(input)));
434
+
435
+ export {
436
+ contextCapsuleVerificationEvidenceSchema,
437
+ contextCapsuleVerificationSchema,
438
+ type ContextCapsuleVerification,
439
+ } from "./context-capsule-verification";