@gmickel/gno 1.19.0 → 1.21.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.
- package/README.md +28 -8
- package/assets/skill/SKILL.md +73 -27
- package/assets/skill/mcp-reference.md +7 -2
- package/assets/skill/recipes/citation-and-provenance.md +32 -9
- package/package.json +1 -1
- package/spec/cli.md +142 -17
- package/spec/db/schema.sql +170 -0
- package/spec/evals-agentic.md +87 -5
- package/spec/mcp.md +75 -3
- package/spec/output-schemas/ask.schema.json +198 -0
- package/spec/output-schemas/capsule-reverified-event.schema.json +47 -0
- package/spec/output-schemas/changes.schema.json +280 -0
- package/spec/output-schemas/claim-verification.schema.json +291 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +36 -1
- package/spec/output-schemas/document-diff.schema.json +185 -0
- package/spec/output-schemas/impact.schema.json +122 -0
- package/spec/output-schemas/saved-capsule-list.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-registration.schema.json +172 -0
- package/spec/output-schemas/saved-capsule-reverification.schema.json +59 -0
- package/spec/output-schemas/saved-capsule-unwatch.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-watch.schema.json +17 -0
- package/src/app/context-runtime-contract.ts +10 -5
- package/src/app/context-runtime-input.ts +29 -1
- package/src/app/context-runtime-types.ts +4 -0
- package/src/app/context-runtime.ts +5 -1
- package/src/app/context-surface.ts +4 -0
- package/src/app/verified-ask.ts +291 -0
- package/src/cli/commands/ask-format.ts +255 -0
- package/src/cli/commands/ask.ts +40 -149
- package/src/cli/commands/changes.ts +160 -0
- package/src/cli/commands/context-saved.ts +189 -0
- package/src/cli/options.ts +8 -0
- package/src/cli/program.ts +227 -1
- package/src/core/capsule-registry.ts +279 -0
- package/src/core/capsule-reverification-scheduler.ts +218 -0
- package/src/core/capsule-reverification.ts +289 -0
- package/src/core/change-diff.ts +182 -0
- package/src/core/change-journal.ts +228 -0
- package/src/core/context-budget.ts +6 -0
- package/src/core/context-capsule-retrieval-schema.ts +4 -0
- package/src/core/context-capsule-schema.ts +17 -0
- package/src/core/context-capsule-validation.ts +3 -2
- package/src/core/context-capsule.ts +18 -0
- package/src/core/context-compiler.ts +33 -21
- package/src/core/context-evidence.ts +6 -0
- package/src/core/knowledge-delta.ts +395 -0
- package/src/core/knowledge-impact.ts +202 -0
- package/src/core/retrieval-trace-evidence-origin.ts +3 -0
- package/src/core/retrieval-trace-session.ts +15 -2
- package/src/ingestion/sync.ts +214 -165
- package/src/llm/errors.ts +10 -1
- package/src/llm/httpGeneration.ts +11 -1
- package/src/llm/nodeLlamaCpp/generation.ts +54 -10
- package/src/llm/types.ts +6 -0
- package/src/mcp/tools/ask.ts +228 -0
- package/src/mcp/tools/changes.ts +80 -0
- package/src/mcp/tools/context.ts +28 -7
- package/src/mcp/tools/index.ts +38 -0
- package/src/pipeline/claim-verification-schema.ts +235 -0
- package/src/pipeline/claim-verification.ts +487 -0
- package/src/pipeline/claim-verifier.ts +474 -0
- package/src/pipeline/types.ts +25 -0
- package/src/sdk/client.ts +77 -2
- package/src/sdk/index.ts +7 -0
- package/src/sdk/types.ts +22 -0
- package/src/serve/doc-events.ts +12 -1
- package/src/serve/public/components/AskVerificationPanel.tsx +189 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Ask.tsx +42 -4
- package/src/serve/resident-runtime.ts +22 -0
- package/src/serve/routes/api.ts +162 -3
- package/src/serve/routes/changes.ts +102 -0
- package/src/serve/server.ts +34 -0
- package/src/serve/watch-service.ts +9 -0
- package/src/store/index.ts +21 -0
- package/src/store/migrations/015-document-change-journal.ts +85 -0
- package/src/store/migrations/016-saved-capsules.ts +131 -0
- package/src/store/migrations/017-document-change-retention-counters.ts +33 -0
- package/src/store/migrations/018-saved-capsule-registration-epoch.ts +24 -0
- package/src/store/migrations/019-saved-capsule-registration-generation.ts +53 -0
- package/src/store/migrations/index.ts +10 -0
- package/src/store/sqlite/adapter.ts +291 -7
- package/src/store/sqlite/capsule-registry-store.ts +534 -0
- package/src/store/sqlite/change-journal-store.ts +473 -0
- package/src/store/types.ts +262 -0
package/src/cli/program.ts
CHANGED
|
@@ -308,6 +308,7 @@ export function createProgram(): Command {
|
|
|
308
308
|
wireTagsCommands(program);
|
|
309
309
|
wireLinksCommands(program);
|
|
310
310
|
wireGraphCommand(program);
|
|
311
|
+
wireKnowledgeDeltaCommands(program);
|
|
311
312
|
wireMcpCommand(program);
|
|
312
313
|
wireSkillCommands(program);
|
|
313
314
|
wireDaemonCommand(program);
|
|
@@ -635,7 +636,9 @@ function wireSearchCommands(program: Command): void {
|
|
|
635
636
|
|
|
636
637
|
const limit = cmdOpts.limit
|
|
637
638
|
? parsePositiveInt("limit", cmdOpts.limit)
|
|
638
|
-
:
|
|
639
|
+
: cmdOpts.verify
|
|
640
|
+
? 5
|
|
641
|
+
: getDefaultLimit(format);
|
|
639
642
|
const categories = parseCsvValues(cmdOpts.category);
|
|
640
643
|
const exclude = parseCsvValues(cmdOpts.exclude);
|
|
641
644
|
|
|
@@ -1108,8 +1111,16 @@ function wireSearchCommands(program: Command): void {
|
|
|
1108
1111
|
)
|
|
1109
1112
|
.option("-C, --candidate-limit <num>", "max candidates passed to reranking")
|
|
1110
1113
|
.option("--answer", "generate short grounded answer")
|
|
1114
|
+
.option(
|
|
1115
|
+
"--verify",
|
|
1116
|
+
"generate and verify every claim against a closed Context Capsule"
|
|
1117
|
+
)
|
|
1111
1118
|
.option("--no-answer", "force retrieval-only output")
|
|
1112
1119
|
.option("--max-answer-tokens <num>", "max answer tokens")
|
|
1120
|
+
.option("--context-budget-tokens <num>", "verified Context token budget")
|
|
1121
|
+
.option("--context-budget-bytes <num>", "verified Context byte budget")
|
|
1122
|
+
.option("--min-score <score>", "minimum retrieval score (0-1)")
|
|
1123
|
+
.option("--graph", "include bounded graph expansion")
|
|
1113
1124
|
.option("--show-sources", "show all retrieved sources (not just cited)")
|
|
1114
1125
|
.option("--json", "JSON output")
|
|
1115
1126
|
.option("--md", "Markdown output")
|
|
@@ -1140,6 +1151,22 @@ function wireSearchCommands(program: Command): void {
|
|
|
1140
1151
|
const maxAnswerTokens = cmdOpts.maxAnswerTokens
|
|
1141
1152
|
? parsePositiveInt("max-answer-tokens", cmdOpts.maxAnswerTokens)
|
|
1142
1153
|
: undefined;
|
|
1154
|
+
const contextBudgetTokens = cmdOpts.contextBudgetTokens
|
|
1155
|
+
? parsePositiveInt("context-budget-tokens", cmdOpts.contextBudgetTokens)
|
|
1156
|
+
: undefined;
|
|
1157
|
+
const contextBudgetBytes = cmdOpts.contextBudgetBytes
|
|
1158
|
+
? parsePositiveInt("context-budget-bytes", cmdOpts.contextBudgetBytes)
|
|
1159
|
+
: undefined;
|
|
1160
|
+
const minScore = parseOptionalFloat("min-score", cmdOpts.minScore);
|
|
1161
|
+
if (minScore !== undefined && (minScore < 0 || minScore > 1)) {
|
|
1162
|
+
throw new CliError("VALIDATION", "min-score must be between 0 and 1");
|
|
1163
|
+
}
|
|
1164
|
+
if (cmdOpts.verify && cmdOpts.noAnswer) {
|
|
1165
|
+
throw new CliError(
|
|
1166
|
+
"VALIDATION",
|
|
1167
|
+
"--verify cannot be combined with --no-answer"
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1143
1170
|
const categories = parseCsvValues(cmdOpts.category);
|
|
1144
1171
|
const exclude = parseCsvValues(cmdOpts.exclude);
|
|
1145
1172
|
|
|
@@ -1188,6 +1215,8 @@ function wireSearchCommands(program: Command): void {
|
|
|
1188
1215
|
author: cmdOpts.author as string | undefined,
|
|
1189
1216
|
intent: cmdOpts.intent as string | undefined,
|
|
1190
1217
|
exclude,
|
|
1218
|
+
minScore,
|
|
1219
|
+
graph: Boolean(cmdOpts.graph),
|
|
1191
1220
|
queryModes,
|
|
1192
1221
|
noExpand: depthPolicy.noExpand,
|
|
1193
1222
|
noRerank: depthPolicy.noRerank,
|
|
@@ -1196,7 +1225,10 @@ function wireSearchCommands(program: Command): void {
|
|
|
1196
1225
|
// Commander creates separate cmdOpts.noAnswer for --no-answer flag
|
|
1197
1226
|
answer: Boolean(cmdOpts.answer),
|
|
1198
1227
|
noAnswer: Boolean(cmdOpts.noAnswer),
|
|
1228
|
+
verify: Boolean(cmdOpts.verify),
|
|
1199
1229
|
maxAnswerTokens,
|
|
1230
|
+
contextBudgetTokens,
|
|
1231
|
+
contextBudgetBytes,
|
|
1200
1232
|
showSources,
|
|
1201
1233
|
json: format === "json",
|
|
1202
1234
|
md: format === "md",
|
|
@@ -2057,6 +2089,106 @@ function wireManagementCommands(program: Command): void {
|
|
|
2057
2089
|
}
|
|
2058
2090
|
);
|
|
2059
2091
|
|
|
2092
|
+
contextCmd
|
|
2093
|
+
.command("watch <file>")
|
|
2094
|
+
.description("Watch a saved Context Capsule for evidence changes")
|
|
2095
|
+
.option("--question <text>", "question associated with this Capsule")
|
|
2096
|
+
.option("--label <text>", "short local label")
|
|
2097
|
+
.option("--notify", "emit metadata-only local reverification events")
|
|
2098
|
+
.option("--json", "JSON output")
|
|
2099
|
+
.action(
|
|
2100
|
+
async (
|
|
2101
|
+
file: string,
|
|
2102
|
+
cmdOpts: Record<string, unknown>,
|
|
2103
|
+
command: Command
|
|
2104
|
+
) => {
|
|
2105
|
+
const format = getFormat(cmdOpts);
|
|
2106
|
+
assertFormatSupported(CMD.contextSaved, format);
|
|
2107
|
+
const globals = getGlobals();
|
|
2108
|
+
const explicitIndexName =
|
|
2109
|
+
command.getOptionValueSourceWithGlobals("index") === "cli"
|
|
2110
|
+
? globals.index
|
|
2111
|
+
: undefined;
|
|
2112
|
+
const { watchSavedCapsule } = await import("./commands/context-saved");
|
|
2113
|
+
await writeOutput(
|
|
2114
|
+
await watchSavedCapsule(file, {
|
|
2115
|
+
configPath: globals.config,
|
|
2116
|
+
indexName: globals.index,
|
|
2117
|
+
explicitIndexName,
|
|
2118
|
+
question: cmdOpts.question as string | undefined,
|
|
2119
|
+
label: cmdOpts.label as string | undefined,
|
|
2120
|
+
notify: Boolean(cmdOpts.notify),
|
|
2121
|
+
format: format === "json" ? "json" : "terminal",
|
|
2122
|
+
}),
|
|
2123
|
+
format === "json" ? "json" : "terminal"
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
2126
|
+
);
|
|
2127
|
+
|
|
2128
|
+
contextCmd
|
|
2129
|
+
.command("watches")
|
|
2130
|
+
.description("List watched saved Context Capsules")
|
|
2131
|
+
.option("--json", "JSON output")
|
|
2132
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
2133
|
+
const format = getFormat(cmdOpts);
|
|
2134
|
+
assertFormatSupported(CMD.contextSaved, format);
|
|
2135
|
+
const globals = getGlobals();
|
|
2136
|
+
const { listWatchedCapsules } = await import("./commands/context-saved");
|
|
2137
|
+
await writeOutput(
|
|
2138
|
+
await listWatchedCapsules({
|
|
2139
|
+
configPath: globals.config,
|
|
2140
|
+
indexName: globals.index,
|
|
2141
|
+
format: format === "json" ? "json" : "terminal",
|
|
2142
|
+
}),
|
|
2143
|
+
format === "json" ? "json" : "terminal"
|
|
2144
|
+
);
|
|
2145
|
+
});
|
|
2146
|
+
|
|
2147
|
+
contextCmd
|
|
2148
|
+
.command("unwatch <registration>")
|
|
2149
|
+
.description("Stop watching a saved Context Capsule")
|
|
2150
|
+
.option("--json", "JSON output")
|
|
2151
|
+
.action(async (registration: string, cmdOpts: Record<string, unknown>) => {
|
|
2152
|
+
const format = getFormat(cmdOpts);
|
|
2153
|
+
assertFormatSupported(CMD.contextSaved, format);
|
|
2154
|
+
const globals = getGlobals();
|
|
2155
|
+
const { unwatchSavedCapsule } = await import("./commands/context-saved");
|
|
2156
|
+
await writeOutput(
|
|
2157
|
+
await unwatchSavedCapsule(registration, {
|
|
2158
|
+
configPath: globals.config,
|
|
2159
|
+
indexName: globals.index,
|
|
2160
|
+
format: format === "json" ? "json" : "terminal",
|
|
2161
|
+
}),
|
|
2162
|
+
format === "json" ? "json" : "terminal"
|
|
2163
|
+
);
|
|
2164
|
+
});
|
|
2165
|
+
|
|
2166
|
+
contextCmd
|
|
2167
|
+
.command("reverify <registration>")
|
|
2168
|
+
.description("Reverify one watched saved Context Capsule")
|
|
2169
|
+
.option("--json", "JSON output")
|
|
2170
|
+
.action(async (registration: string, cmdOpts: Record<string, unknown>) => {
|
|
2171
|
+
const format = getFormat(cmdOpts);
|
|
2172
|
+
assertFormatSupported(CMD.contextSaved, format);
|
|
2173
|
+
const globals = getGlobals();
|
|
2174
|
+
const { reverifyWatchedCapsule } =
|
|
2175
|
+
await import("./commands/context-saved");
|
|
2176
|
+
const result = await reverifyWatchedCapsule(registration, {
|
|
2177
|
+
configPath: globals.config,
|
|
2178
|
+
indexName: globals.index,
|
|
2179
|
+
format: format === "json" ? "json" : "terminal",
|
|
2180
|
+
});
|
|
2181
|
+
await writeOutput(result.output, format === "json" ? "json" : "terminal");
|
|
2182
|
+
if (result.operationStatus === "failed") {
|
|
2183
|
+
throw new CliError(
|
|
2184
|
+
"RUNTIME",
|
|
2185
|
+
result.errorMessage ??
|
|
2186
|
+
"Saved Context Capsule verification operation failed",
|
|
2187
|
+
{ operationStatus: "failed" }
|
|
2188
|
+
);
|
|
2189
|
+
}
|
|
2190
|
+
});
|
|
2191
|
+
|
|
2060
2192
|
contextCmd
|
|
2061
2193
|
.command("rm <uri>")
|
|
2062
2194
|
.description("Remove context item")
|
|
@@ -3057,6 +3189,100 @@ function wireGraphCommand(program: Command): void {
|
|
|
3057
3189
|
);
|
|
3058
3190
|
}
|
|
3059
3191
|
|
|
3192
|
+
function wireKnowledgeDeltaCommands(program: Command): void {
|
|
3193
|
+
program
|
|
3194
|
+
.command("changes")
|
|
3195
|
+
.description("List retained metadata-only document changes")
|
|
3196
|
+
.option("--since <time-or-cursor>", "ISO-8601 time or opaque cursor")
|
|
3197
|
+
.option("-c, --collection <name>", "filter by collection")
|
|
3198
|
+
.option("-n, --limit <num>", "maximum changes", "100")
|
|
3199
|
+
.option("--json", "JSON output")
|
|
3200
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
3201
|
+
const format = getFormat(cmdOpts);
|
|
3202
|
+
assertFormatSupported(CMD.changes, format);
|
|
3203
|
+
const deltaFormat = format === "json" ? "json" : "terminal";
|
|
3204
|
+
const globals = getGlobals();
|
|
3205
|
+
const { changes, formatChanges } = await import("./commands/changes");
|
|
3206
|
+
const result = await changes(
|
|
3207
|
+
{
|
|
3208
|
+
since: cmdOpts.since as string | undefined,
|
|
3209
|
+
collection: cmdOpts.collection as string | undefined,
|
|
3210
|
+
limit: parsePositiveInt("limit", cmdOpts.limit),
|
|
3211
|
+
},
|
|
3212
|
+
{ configPath: globals.config, indexName: globals.index }
|
|
3213
|
+
);
|
|
3214
|
+
if (!result.success) {
|
|
3215
|
+
throw new CliError(
|
|
3216
|
+
result.isValidation ? "VALIDATION" : "RUNTIME",
|
|
3217
|
+
result.error
|
|
3218
|
+
);
|
|
3219
|
+
}
|
|
3220
|
+
await writeOutput(formatChanges(result.data, deltaFormat), deltaFormat);
|
|
3221
|
+
});
|
|
3222
|
+
|
|
3223
|
+
program
|
|
3224
|
+
.command("diff <doc>")
|
|
3225
|
+
.description("Show one retained metadata-only structural change")
|
|
3226
|
+
.option("--change <id>", "opaque change ID")
|
|
3227
|
+
.option("--json", "JSON output")
|
|
3228
|
+
.action(async (doc: string, cmdOpts: Record<string, unknown>) => {
|
|
3229
|
+
const format = getFormat(cmdOpts);
|
|
3230
|
+
assertFormatSupported(CMD.diff, format);
|
|
3231
|
+
const deltaFormat = format === "json" ? "json" : "terminal";
|
|
3232
|
+
const globals = getGlobals();
|
|
3233
|
+
const { diff, formatDiff } = await import("./commands/changes");
|
|
3234
|
+
const result = await diff(doc, cmdOpts.change as string | undefined, {
|
|
3235
|
+
configPath: globals.config,
|
|
3236
|
+
indexName: globals.index,
|
|
3237
|
+
});
|
|
3238
|
+
if (!result.success) {
|
|
3239
|
+
throw new CliError(
|
|
3240
|
+
result.isValidation ? "VALIDATION" : "RUNTIME",
|
|
3241
|
+
result.error
|
|
3242
|
+
);
|
|
3243
|
+
}
|
|
3244
|
+
await writeOutput(formatDiff(result.data, deltaFormat), deltaFormat);
|
|
3245
|
+
});
|
|
3246
|
+
|
|
3247
|
+
program
|
|
3248
|
+
.command("impact <doc>")
|
|
3249
|
+
.description("Find bounded inbound knowledge dependencies")
|
|
3250
|
+
.option("--max-depth <n>", "maximum dependency depth", "3")
|
|
3251
|
+
.option("--max-nodes <n>", "maximum returned nodes", "100")
|
|
3252
|
+
.option("--max-edges <n>", "maximum traversed evidence edges", "250")
|
|
3253
|
+
.option("--frontier-limit <n>", "maximum frontier width", "100")
|
|
3254
|
+
.option("--visited-limit <n>", "maximum visited rows", "500")
|
|
3255
|
+
.option("--json", "JSON output")
|
|
3256
|
+
.action(async (doc: string, cmdOpts: Record<string, unknown>) => {
|
|
3257
|
+
const format = getFormat(cmdOpts);
|
|
3258
|
+
assertFormatSupported(CMD.impact, format);
|
|
3259
|
+
const deltaFormat = format === "json" ? "json" : "terminal";
|
|
3260
|
+
const globals = getGlobals();
|
|
3261
|
+
const { impact, formatImpact } = await import("./commands/changes");
|
|
3262
|
+
const result = await impact(
|
|
3263
|
+
doc,
|
|
3264
|
+
{
|
|
3265
|
+
maxDepth: parsePositiveInt("max-depth", cmdOpts.maxDepth),
|
|
3266
|
+
maxNodes: parsePositiveInt("max-nodes", cmdOpts.maxNodes),
|
|
3267
|
+
maxEdges: parsePositiveInt("max-edges", cmdOpts.maxEdges),
|
|
3268
|
+
frontierLimit: parsePositiveInt(
|
|
3269
|
+
"frontier-limit",
|
|
3270
|
+
cmdOpts.frontierLimit
|
|
3271
|
+
),
|
|
3272
|
+
visitedLimit: parsePositiveInt("visited-limit", cmdOpts.visitedLimit),
|
|
3273
|
+
},
|
|
3274
|
+
{ configPath: globals.config, indexName: globals.index }
|
|
3275
|
+
);
|
|
3276
|
+
if (!result.success) {
|
|
3277
|
+
throw new CliError(
|
|
3278
|
+
result.isValidation ? "VALIDATION" : "RUNTIME",
|
|
3279
|
+
result.error
|
|
3280
|
+
);
|
|
3281
|
+
}
|
|
3282
|
+
await writeOutput(formatImpact(result.data, deltaFormat), deltaFormat);
|
|
3283
|
+
});
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3060
3286
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
3061
3287
|
// Serve Command (web UI)
|
|
3062
3288
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/** Metadata-only registry for explicitly saved Context Capsule files. */
|
|
2
|
+
|
|
3
|
+
// node:path resolve has no Bun path utility equivalent.
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
SavedCapsuleNotificationPreference,
|
|
8
|
+
SavedCapsuleRegistrationRecord,
|
|
9
|
+
StorePort,
|
|
10
|
+
StoreResult,
|
|
11
|
+
} from "../store/types";
|
|
12
|
+
import type { ContextCapsuleV1 } from "./context-capsule";
|
|
13
|
+
|
|
14
|
+
import { DEFAULT_INDEX_NAME, stripUriIndex } from "../app/constants";
|
|
15
|
+
import { canonicalizeIndexName } from "../app/index-name";
|
|
16
|
+
import { decodeDocumentChangeCursor } from "./change-journal";
|
|
17
|
+
import { sha256Text } from "./context-capsule-validation";
|
|
18
|
+
import { parseCanonicalContextCapsuleForVerification } from "./context-verifier";
|
|
19
|
+
import { canonicalVerifierJson } from "./context-verifier-canonical";
|
|
20
|
+
|
|
21
|
+
const MAX_CAPSULE_BYTES = 16 * 1024 * 1024;
|
|
22
|
+
const MAX_EVIDENCE_REFERENCES = 10_000;
|
|
23
|
+
const MAX_QUESTION_BYTES = 8192;
|
|
24
|
+
const MAX_LABEL_BYTES = 512;
|
|
25
|
+
const UTF8_ENCODER = new TextEncoder();
|
|
26
|
+
const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
27
|
+
|
|
28
|
+
type RegistryStore = Pick<
|
|
29
|
+
StorePort,
|
|
30
|
+
| "deleteSavedCapsuleRegistration"
|
|
31
|
+
| "getSavedCapsuleRegistration"
|
|
32
|
+
| "listDocumentChanges"
|
|
33
|
+
| "listSavedCapsuleRegistrations"
|
|
34
|
+
| "upsertSavedCapsuleRegistration"
|
|
35
|
+
>;
|
|
36
|
+
|
|
37
|
+
export type SavedCapsuleRegistryErrorCode =
|
|
38
|
+
| "capsule_file_changed"
|
|
39
|
+
| "capsule_file_missing"
|
|
40
|
+
| "capsule_file_too_large"
|
|
41
|
+
| "capsule_read_failed"
|
|
42
|
+
| "invalid_filter"
|
|
43
|
+
| "invalid_metadata"
|
|
44
|
+
| "registration_not_found"
|
|
45
|
+
| "store_failed";
|
|
46
|
+
|
|
47
|
+
export class SavedCapsuleRegistryError extends Error {
|
|
48
|
+
readonly code: SavedCapsuleRegistryErrorCode;
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
code: SavedCapsuleRegistryErrorCode,
|
|
52
|
+
message: string,
|
|
53
|
+
cause?: unknown
|
|
54
|
+
) {
|
|
55
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
56
|
+
this.name = "SavedCapsuleRegistryError";
|
|
57
|
+
this.code = code;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RegisterSavedCapsuleInput {
|
|
62
|
+
filePath: string;
|
|
63
|
+
question?: string;
|
|
64
|
+
label?: string;
|
|
65
|
+
notificationPreference?: SavedCapsuleNotificationPreference;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface LoadedSavedCapsule {
|
|
69
|
+
capsule: ContextCapsuleV1;
|
|
70
|
+
fileHash: string;
|
|
71
|
+
filePath: string;
|
|
72
|
+
raw: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const unwrapStore = <T>(result: StoreResult<T>, operation: string): T => {
|
|
76
|
+
if (result.ok) return result.value;
|
|
77
|
+
throw new SavedCapsuleRegistryError(
|
|
78
|
+
"store_failed",
|
|
79
|
+
`${operation}: ${result.error.message}`,
|
|
80
|
+
result.error.cause
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const boundedOptionalText = (
|
|
85
|
+
value: string | undefined,
|
|
86
|
+
field: "question" | "label",
|
|
87
|
+
maxBytes: number
|
|
88
|
+
): string | null => {
|
|
89
|
+
if (value === undefined) return null;
|
|
90
|
+
const normalized = value.trim().normalize("NFC");
|
|
91
|
+
if (
|
|
92
|
+
normalized.length === 0 ||
|
|
93
|
+
UTF8_ENCODER.encode(normalized).byteLength > maxBytes
|
|
94
|
+
) {
|
|
95
|
+
throw new SavedCapsuleRegistryError(
|
|
96
|
+
"invalid_metadata",
|
|
97
|
+
`${field} must be non-empty and at most ${maxBytes} UTF-8 bytes`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return normalized;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const loadSavedCapsuleFile = async (
|
|
104
|
+
filePath: string,
|
|
105
|
+
expectedFileHash?: string
|
|
106
|
+
): Promise<LoadedSavedCapsule> => {
|
|
107
|
+
const canonicalPath = resolve(filePath);
|
|
108
|
+
const file = Bun.file(canonicalPath);
|
|
109
|
+
if (!(await file.exists())) {
|
|
110
|
+
throw new SavedCapsuleRegistryError(
|
|
111
|
+
"capsule_file_missing",
|
|
112
|
+
"Saved Context Capsule file is missing"
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (file.size < 1 || file.size > MAX_CAPSULE_BYTES) {
|
|
116
|
+
throw new SavedCapsuleRegistryError(
|
|
117
|
+
"capsule_file_too_large",
|
|
118
|
+
`Saved Context Capsule must be between 1 and ${MAX_CAPSULE_BYTES} bytes`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const raw = UTF8_DECODER.decode(await file.arrayBuffer());
|
|
123
|
+
const fileHash = sha256Text(raw);
|
|
124
|
+
if (expectedFileHash !== undefined && fileHash !== expectedFileHash) {
|
|
125
|
+
throw new SavedCapsuleRegistryError(
|
|
126
|
+
"capsule_file_changed",
|
|
127
|
+
"Saved Context Capsule file changed after registration"
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const capsule = parseCanonicalContextCapsuleForVerification(
|
|
131
|
+
JSON.parse(raw) as unknown
|
|
132
|
+
);
|
|
133
|
+
if (capsule.evidence.length > MAX_EVIDENCE_REFERENCES) {
|
|
134
|
+
throw new SavedCapsuleRegistryError(
|
|
135
|
+
"capsule_file_too_large",
|
|
136
|
+
`Saved Context Capsule exceeds ${MAX_EVIDENCE_REFERENCES} evidence references`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
capsule,
|
|
141
|
+
fileHash,
|
|
142
|
+
filePath: canonicalPath,
|
|
143
|
+
raw,
|
|
144
|
+
};
|
|
145
|
+
} catch (cause) {
|
|
146
|
+
if (cause instanceof SavedCapsuleRegistryError) throw cause;
|
|
147
|
+
throw new SavedCapsuleRegistryError(
|
|
148
|
+
"capsule_read_failed",
|
|
149
|
+
cause instanceof Error
|
|
150
|
+
? `Saved Context Capsule is invalid: ${cause.message}`
|
|
151
|
+
: "Saved Context Capsule is invalid",
|
|
152
|
+
cause
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const assertRuntimeIndex = (
|
|
158
|
+
capsule: ContextCapsuleV1,
|
|
159
|
+
runtimeIndexName: string
|
|
160
|
+
): string => {
|
|
161
|
+
const effective = canonicalizeIndexName(
|
|
162
|
+
runtimeIndexName || DEFAULT_INDEX_NAME
|
|
163
|
+
);
|
|
164
|
+
if (effective !== capsule.scope.indexName) {
|
|
165
|
+
throw new SavedCapsuleRegistryError(
|
|
166
|
+
"invalid_filter",
|
|
167
|
+
`Context Capsule index ${capsule.scope.indexName} does not match runtime index ${effective}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return effective;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const latestSequence = async (store: RegistryStore): Promise<number> => {
|
|
174
|
+
const page = unwrapStore(
|
|
175
|
+
await store.listDocumentChanges({ limit: 1 }),
|
|
176
|
+
"Failed to read the document change journal"
|
|
177
|
+
);
|
|
178
|
+
return decodeDocumentChangeCursor(page.latestCursor);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/** Register an explicit file without persisting or rewriting its body. */
|
|
182
|
+
export const registerSavedCapsule = async (
|
|
183
|
+
store: RegistryStore,
|
|
184
|
+
runtimeIndexName: string,
|
|
185
|
+
input: RegisterSavedCapsuleInput,
|
|
186
|
+
nowMs: number = Date.now()
|
|
187
|
+
): Promise<SavedCapsuleRegistrationRecord> => {
|
|
188
|
+
// Capture the conservative high-water mark before reading the caller-owned
|
|
189
|
+
// file. Any journal change concurrent with file loading then remains newer
|
|
190
|
+
// than the registration and cannot be skipped by the resident scheduler.
|
|
191
|
+
const sequence = await latestSequence(store);
|
|
192
|
+
const loaded = await loadSavedCapsuleFile(input.filePath);
|
|
193
|
+
const indexName = assertRuntimeIndex(loaded.capsule, runtimeIndexName);
|
|
194
|
+
const registrationId = `capsule-${sha256Text(loaded.filePath).slice(0, 40)}`;
|
|
195
|
+
const existing = unwrapStore(
|
|
196
|
+
await store.getSavedCapsuleRegistration(registrationId),
|
|
197
|
+
"Failed to read saved Context Capsule registration"
|
|
198
|
+
);
|
|
199
|
+
return unwrapStore(
|
|
200
|
+
await store.upsertSavedCapsuleRegistration({
|
|
201
|
+
registrationId,
|
|
202
|
+
filePath: loaded.filePath,
|
|
203
|
+
fileHash: loaded.fileHash,
|
|
204
|
+
capsuleId: loaded.capsule.capsuleId,
|
|
205
|
+
indexName,
|
|
206
|
+
question: boundedOptionalText(
|
|
207
|
+
input.question,
|
|
208
|
+
"question",
|
|
209
|
+
MAX_QUESTION_BYTES
|
|
210
|
+
),
|
|
211
|
+
label: boundedOptionalText(input.label, "label", MAX_LABEL_BYTES),
|
|
212
|
+
notificationPreference: input.notificationPreference ?? "none",
|
|
213
|
+
registeredAtMs: existing?.registeredAtMs ?? nowMs,
|
|
214
|
+
updatedAtMs: nowMs,
|
|
215
|
+
lastAttemptedSequence: sequence,
|
|
216
|
+
evidence: loaded.capsule.evidence
|
|
217
|
+
.map((evidence) => ({
|
|
218
|
+
evidenceId: evidence.evidenceId,
|
|
219
|
+
canonicalUri: stripUriIndex(evidence.uri),
|
|
220
|
+
collection: evidence.collection,
|
|
221
|
+
sourceHash: evidence.sourceHash,
|
|
222
|
+
mirrorHash: evidence.mirrorHash,
|
|
223
|
+
passageHash: evidence.passageHash,
|
|
224
|
+
}))
|
|
225
|
+
.sort((left, right) =>
|
|
226
|
+
left.evidenceId < right.evidenceId
|
|
227
|
+
? -1
|
|
228
|
+
: left.evidenceId > right.evidenceId
|
|
229
|
+
? 1
|
|
230
|
+
: 0
|
|
231
|
+
),
|
|
232
|
+
}),
|
|
233
|
+
"Failed to register saved Context Capsule"
|
|
234
|
+
);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export const listSavedCapsules = async (
|
|
238
|
+
store: RegistryStore
|
|
239
|
+
): Promise<SavedCapsuleRegistrationRecord[]> =>
|
|
240
|
+
unwrapStore(
|
|
241
|
+
await store.listSavedCapsuleRegistrations(),
|
|
242
|
+
"Failed to list saved Context Capsules"
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
export const getSavedCapsule = async (
|
|
246
|
+
store: RegistryStore,
|
|
247
|
+
registrationId: string
|
|
248
|
+
): Promise<SavedCapsuleRegistrationRecord> => {
|
|
249
|
+
const registration = unwrapStore(
|
|
250
|
+
await store.getSavedCapsuleRegistration(registrationId),
|
|
251
|
+
"Failed to read saved Context Capsule"
|
|
252
|
+
);
|
|
253
|
+
if (!registration) {
|
|
254
|
+
throw new SavedCapsuleRegistryError(
|
|
255
|
+
"registration_not_found",
|
|
256
|
+
"Saved Context Capsule registration not found"
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
return registration;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export const unregisterSavedCapsule = async (
|
|
263
|
+
store: RegistryStore,
|
|
264
|
+
registrationId: string
|
|
265
|
+
): Promise<void> => {
|
|
266
|
+
const deleted = unwrapStore(
|
|
267
|
+
await store.deleteSavedCapsuleRegistration(registrationId),
|
|
268
|
+
"Failed to remove saved Context Capsule"
|
|
269
|
+
);
|
|
270
|
+
if (!deleted) {
|
|
271
|
+
throw new SavedCapsuleRegistryError(
|
|
272
|
+
"registration_not_found",
|
|
273
|
+
"Saved Context Capsule registration not found"
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
export const canonicalSavedCapsuleRegistryJson = (value: unknown): string =>
|
|
279
|
+
canonicalVerifierJson(value);
|