@gmickel/gno 2.3.3 → 2.5.1
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 +22 -3
- package/assets/skill/SKILL.md +53 -0
- package/assets/skill/cli-reference.md +29 -0
- package/assets/skill/examples.md +30 -0
- package/assets/skill/mcp-reference.md +26 -0
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.3.3.zip → gno-browser-clipper-v2.5.1.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.5.1.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +41 -0
- package/spec/compiled-context.md +68 -0
- package/spec/db/schema.sql +2 -0
- package/spec/evals.md +48 -0
- package/spec/mcp.md +37 -0
- package/spec/output-schemas/ask.schema.json +12 -0
- package/spec/output-schemas/compiled-context-check.schema.json +44 -0
- package/spec/output-schemas/compiled-context-file.schema.json +165 -0
- package/spec/output-schemas/compiled-context-preview.schema.json +142 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +205 -32
- package/spec/output-schemas/context-capsule-verification.schema.json +32 -9
- package/spec/output-schemas/evidence-fixtures.schema.json +176 -0
- package/spec/output-schemas/evidence-report.schema.json +322 -0
- package/spec/output-schemas/evidence-run.schema.json +277 -0
- package/spec/output-schemas/metadata-predicate.schema.json +460 -0
- package/spec/output-schemas/retrieval-trace-filters.schema.json +13 -4
- package/spec/output-schemas/search-results.schema.json +12 -0
- package/spec/output-schemas/status.schema.json +37 -6
- package/src/app/compiled-context-files.ts +361 -0
- package/src/app/compiled-context.ts +240 -0
- package/src/app/context-runtime-contract.ts +33 -2
- package/src/app/context-runtime-input.ts +17 -0
- package/src/app/context-runtime-types.ts +2 -0
- package/src/app/context-runtime.ts +27 -3
- package/src/app/context-surface.ts +7 -0
- package/src/app/verified-ask.ts +1 -0
- package/src/cli/commands/ask.ts +1 -0
- package/src/cli/commands/audit.ts +4 -0
- package/src/cli/commands/context-build.ts +1 -0
- package/src/cli/commands/context-compiled.ts +136 -0
- package/src/cli/commands/search.ts +1 -0
- package/src/cli/commands/status.ts +10 -0
- package/src/cli/errors.ts +10 -3
- package/src/cli/options.ts +19 -0
- package/src/cli/program.ts +82 -0
- package/src/converters/types.ts +2 -0
- package/src/core/compiled-context.ts +254 -0
- package/src/core/context-budget.ts +2 -10
- package/src/core/context-capsule-retrieval-schema.ts +8 -0
- package/src/core/context-capsule-schema.ts +47 -3
- package/src/core/context-capsule-validation.ts +14 -2
- package/src/core/context-capsule-verification.ts +1 -0
- package/src/core/context-capsule.ts +16 -2
- package/src/core/context-compiler.ts +3 -0
- package/src/core/file-lock.ts +22 -5
- package/src/core/folder-setup-planning.ts +2 -1
- package/src/core/network-boundary-inventory.ts +8 -0
- package/src/core/record-metadata.ts +1 -1
- package/src/core/retrieval-replay-candidate.ts +5 -0
- package/src/core/retrieval-trace-filter-normalization.ts +4 -0
- package/src/core/retrieval-trace-request.ts +1 -0
- package/src/core/retrieval-trace-session.ts +5 -1
- package/src/core/retrieval-trace.ts +2 -0
- package/src/core/setup-receipt.ts +27 -20
- package/src/core/typed-metadata.ts +228 -0
- package/src/core/validation.ts +9 -2
- package/src/core/windows-private-path.ts +96 -0
- package/src/index.ts +2 -2
- package/src/ingestion/compiled-context.ts +15 -0
- package/src/ingestion/record-adapter-canonical.ts +13 -2
- package/src/ingestion/record-container.ts +9 -0
- package/src/ingestion/sync.ts +51 -9
- package/src/ingestion/typed-metadata.ts +55 -0
- package/src/ingestion/walker.ts +5 -4
- package/src/llm/nodeLlamaCpp/simulator-install.ts +6 -2
- package/src/mcp/http-egress.ts +11 -3
- package/src/mcp/retrieval-warnings.ts +24 -0
- package/src/mcp/tools/ask.ts +23 -2
- package/src/mcp/tools/context.ts +47 -2
- package/src/mcp/tools/index.ts +58 -1
- package/src/mcp/tools/query.ts +16 -1
- package/src/mcp/tools/search.ts +12 -1
- package/src/mcp/tools/status.ts +4 -0
- package/src/mcp/tools/vsearch.ts +12 -1
- package/src/pipeline/diagnose.ts +6 -0
- package/src/pipeline/filters.ts +65 -0
- package/src/pipeline/graph-retrieval.ts +9 -1
- package/src/pipeline/hybrid.ts +22 -2
- package/src/pipeline/search.ts +11 -0
- package/src/pipeline/types.ts +7 -1
- package/src/pipeline/vsearch.ts +21 -1
- package/src/sdk/client.ts +98 -3
- package/src/sdk/index.ts +11 -0
- package/src/sdk/types.ts +35 -1
- package/src/serve/compiled-context.ts +84 -0
- package/src/serve/context-capsule.ts +1 -0
- package/src/serve/public/app.tsx +12 -1
- package/src/serve/public/components/MetadataFilter.tsx +186 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/lib/metadata-filter.ts +28 -0
- package/src/serve/public/lib/retrieval-filters.ts +3 -0
- package/src/serve/public/lib/workspace-tabs.ts +2 -0
- package/src/serve/public/pages/Ask.tsx +48 -3
- package/src/serve/public/pages/CompiledContext.tsx +364 -0
- package/src/serve/public/pages/Dashboard.tsx +7 -0
- package/src/serve/public/pages/DocView.tsx +33 -0
- package/src/serve/public/pages/Search.tsx +44 -1
- package/src/serve/routes/api.ts +50 -0
- package/src/serve/server.ts +43 -2
- package/src/serve/spa-production-build.ts +6 -5
- package/src/store/migrations/030-typed-metadata.ts +13 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +69 -5
- package/src/store/sqlite/eligibility.ts +12 -0
- package/src/store/sqlite/metadata-predicate.ts +71 -0
- package/src/store/types.ts +11 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.3.3.zip.sha256 +0 -1
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// Bun has no lstat, exclusive-create, hard-link or atomic-rename primitives.
|
|
2
|
+
import { lstat, open, link, rename, unlink } from "node:fs/promises";
|
|
3
|
+
import { dirname, parse, resolve, join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
CompiledContextCheck,
|
|
8
|
+
CompiledContextPreview,
|
|
9
|
+
} from "../core/compiled-context";
|
|
10
|
+
import type { ContextCapsuleV1 } from "../core/context-capsule";
|
|
11
|
+
import type { ContextCapsuleBuildInput } from "./context-runtime";
|
|
12
|
+
|
|
13
|
+
import { withWriteLock } from "../core/file-lock";
|
|
14
|
+
import {
|
|
15
|
+
checkCompiledContext,
|
|
16
|
+
compiledContextRefreshRequest,
|
|
17
|
+
previewCompiledContext,
|
|
18
|
+
type CompiledContextRuntimeDeps,
|
|
19
|
+
} from "./compiled-context";
|
|
20
|
+
|
|
21
|
+
const MAX_BYTES = 4 * 1024 * 1024;
|
|
22
|
+
const snapshotSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
capsulePath: z.string().min(1),
|
|
25
|
+
outputDigest: z.string().regex(/^[a-f0-9]{64}$/),
|
|
26
|
+
settings: z
|
|
27
|
+
.object({
|
|
28
|
+
budgetTokens: z.number().int().positive(),
|
|
29
|
+
budgetBytes: z.number().int().positive().optional(),
|
|
30
|
+
})
|
|
31
|
+
.strict(),
|
|
32
|
+
})
|
|
33
|
+
.strict();
|
|
34
|
+
const sidecarSchema = snapshotSchema
|
|
35
|
+
.extend({
|
|
36
|
+
artifactKind: z.literal("gno_compiled_context_sidecar"),
|
|
37
|
+
schemaVersion: z.literal("1.0"),
|
|
38
|
+
rendererVersion: z.literal("1"),
|
|
39
|
+
previous: snapshotSchema.optional(),
|
|
40
|
+
})
|
|
41
|
+
.strict();
|
|
42
|
+
type Snapshot = z.infer<typeof snapshotSchema>;
|
|
43
|
+
type Sidecar = z.infer<typeof sidecarSchema>;
|
|
44
|
+
export interface CompiledContextFileResult {
|
|
45
|
+
status: "written" | "unchanged";
|
|
46
|
+
outputPath: string;
|
|
47
|
+
capsulePath: string;
|
|
48
|
+
sidecarPath: string;
|
|
49
|
+
preview: CompiledContextPreview;
|
|
50
|
+
}
|
|
51
|
+
const digest = (text: string): string =>
|
|
52
|
+
new Bun.CryptoHasher("sha256").update(text).digest("hex");
|
|
53
|
+
const isMissing = (error: unknown): boolean =>
|
|
54
|
+
!!error &&
|
|
55
|
+
typeof error === "object" &&
|
|
56
|
+
"code" in error &&
|
|
57
|
+
error.code === "ENOENT";
|
|
58
|
+
|
|
59
|
+
/** Refuse links in every existing path component; never create parent directories. */
|
|
60
|
+
async function safePath(path: string, allowMissing = false): Promise<string> {
|
|
61
|
+
const absolute = resolve(path);
|
|
62
|
+
const root = parse(absolute).root;
|
|
63
|
+
const parts = absolute.slice(root.length).split(/[\\/]/).filter(Boolean);
|
|
64
|
+
let current = root;
|
|
65
|
+
for (const [index, part] of parts.entries()) {
|
|
66
|
+
current = join(current, part);
|
|
67
|
+
try {
|
|
68
|
+
const stat = await lstat(current);
|
|
69
|
+
if (
|
|
70
|
+
stat.isSymbolicLink() ||
|
|
71
|
+
(index < parts.length - 1 ? !stat.isDirectory() : !stat.isFile())
|
|
72
|
+
) {
|
|
73
|
+
throw new Error(`Unsafe compiled-context path: ${current}`);
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (allowMissing && index === parts.length - 1 && isMissing(error))
|
|
77
|
+
return absolute;
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return absolute;
|
|
82
|
+
}
|
|
83
|
+
async function readBounded(path: string): Promise<string> {
|
|
84
|
+
await safePath(path);
|
|
85
|
+
const file = Bun.file(path);
|
|
86
|
+
if (file.size > MAX_BYTES)
|
|
87
|
+
throw new Error("Compiled-context input exceeds 4 MiB");
|
|
88
|
+
return file.text();
|
|
89
|
+
}
|
|
90
|
+
async function absent(path: string): Promise<void> {
|
|
91
|
+
await safePath(path, true);
|
|
92
|
+
try {
|
|
93
|
+
await lstat(path);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (isMissing(error)) return;
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
throw new Error(`Destination already exists: ${path}`);
|
|
99
|
+
}
|
|
100
|
+
function outputTarget(path: string): string {
|
|
101
|
+
if (!path.endsWith(".gno-context.md"))
|
|
102
|
+
throw new Error("Output must end in .gno-context.md");
|
|
103
|
+
return resolve(path);
|
|
104
|
+
}
|
|
105
|
+
async function stage(path: string, content: string): Promise<string> {
|
|
106
|
+
await safePath(path, true);
|
|
107
|
+
const temp = join(dirname(path), `.${crypto.randomUUID()}.gno-context.tmp`);
|
|
108
|
+
const handle = await open(temp, "wx", 0o600);
|
|
109
|
+
try {
|
|
110
|
+
await Bun.write(Bun.file(handle.fd), content);
|
|
111
|
+
await handle.sync();
|
|
112
|
+
} catch (error) {
|
|
113
|
+
await unlink(temp);
|
|
114
|
+
throw error;
|
|
115
|
+
} finally {
|
|
116
|
+
await handle.close();
|
|
117
|
+
}
|
|
118
|
+
return temp;
|
|
119
|
+
}
|
|
120
|
+
async function removeTemp(path: string): Promise<void> {
|
|
121
|
+
try {
|
|
122
|
+
await unlink(path);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (!isMissing(error)) throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function sidecar(snapshot: Snapshot, previous?: Snapshot): Sidecar {
|
|
128
|
+
return {
|
|
129
|
+
artifactKind: "gno_compiled_context_sidecar",
|
|
130
|
+
schemaVersion: "1.0",
|
|
131
|
+
rendererVersion: "1",
|
|
132
|
+
...snapshot,
|
|
133
|
+
...(previous ? { previous } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
async function owned(
|
|
137
|
+
outputPath: string
|
|
138
|
+
): Promise<{ markdown: string; snapshot: Snapshot; sidecarText: string }> {
|
|
139
|
+
const markdown = await readBounded(outputPath);
|
|
140
|
+
const sidecarText = await readBounded(`${outputPath}.json`);
|
|
141
|
+
const metadata = sidecarSchema.parse(JSON.parse(sidecarText));
|
|
142
|
+
const outputDigest = digest(markdown);
|
|
143
|
+
const snapshot =
|
|
144
|
+
metadata.outputDigest === outputDigest
|
|
145
|
+
? snapshotSchema.parse({
|
|
146
|
+
capsulePath: metadata.capsulePath,
|
|
147
|
+
outputDigest: metadata.outputDigest,
|
|
148
|
+
settings: metadata.settings,
|
|
149
|
+
})
|
|
150
|
+
: metadata.previous;
|
|
151
|
+
if (!snapshot || snapshot.outputDigest !== outputDigest)
|
|
152
|
+
throw new Error("Conflict: compiled context was manually changed");
|
|
153
|
+
return { markdown, snapshot, sidecarText };
|
|
154
|
+
}
|
|
155
|
+
async function unchanged(
|
|
156
|
+
outputPath: string,
|
|
157
|
+
original: Awaited<ReturnType<typeof owned>>
|
|
158
|
+
): Promise<void> {
|
|
159
|
+
if (
|
|
160
|
+
(await readBounded(outputPath)) !== original.markdown ||
|
|
161
|
+
(await readBounded(`${outputPath}.json`)) !== original.sidecarText
|
|
162
|
+
) {
|
|
163
|
+
throw new Error("Conflict: compiled context changed during publication");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function result(
|
|
167
|
+
outputPath: string,
|
|
168
|
+
capsulePath: string,
|
|
169
|
+
preview: CompiledContextPreview,
|
|
170
|
+
status: "written" | "unchanged"
|
|
171
|
+
): CompiledContextFileResult {
|
|
172
|
+
return {
|
|
173
|
+
status,
|
|
174
|
+
outputPath,
|
|
175
|
+
capsulePath,
|
|
176
|
+
sidecarPath: `${outputPath}.json`,
|
|
177
|
+
preview,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function compileContextFile(
|
|
182
|
+
input: {
|
|
183
|
+
capsulePath: string;
|
|
184
|
+
outputPath: string;
|
|
185
|
+
budgetTokens: number;
|
|
186
|
+
budgetBytes?: number;
|
|
187
|
+
},
|
|
188
|
+
deps: CompiledContextRuntimeDeps
|
|
189
|
+
): Promise<CompiledContextFileResult> {
|
|
190
|
+
const outputPath = outputTarget(input.outputPath);
|
|
191
|
+
const capsulePath = await safePath(input.capsulePath);
|
|
192
|
+
await absent(outputPath);
|
|
193
|
+
await absent(`${outputPath}.json`);
|
|
194
|
+
await safePath(`${outputPath}.lock`, true);
|
|
195
|
+
return withWriteLock(`${outputPath}.lock`, async () => {
|
|
196
|
+
await absent(outputPath);
|
|
197
|
+
await absent(`${outputPath}.json`);
|
|
198
|
+
const capsuleText = await readBounded(capsulePath);
|
|
199
|
+
const capsule = JSON.parse(capsuleText);
|
|
200
|
+
const settings = {
|
|
201
|
+
budgetTokens: input.budgetTokens,
|
|
202
|
+
...(input.budgetBytes === undefined
|
|
203
|
+
? {}
|
|
204
|
+
: { budgetBytes: input.budgetBytes }),
|
|
205
|
+
};
|
|
206
|
+
const preview = await previewCompiledContext(
|
|
207
|
+
{ capsule, ...settings },
|
|
208
|
+
deps
|
|
209
|
+
);
|
|
210
|
+
const metadata = sidecar({
|
|
211
|
+
capsulePath,
|
|
212
|
+
outputDigest: preview.digest,
|
|
213
|
+
settings,
|
|
214
|
+
});
|
|
215
|
+
const stagedOutput = await stage(outputPath, preview.markdown);
|
|
216
|
+
let stagedSidecar: string | undefined;
|
|
217
|
+
let publishedSidecar = false;
|
|
218
|
+
try {
|
|
219
|
+
stagedSidecar = await stage(
|
|
220
|
+
`${outputPath}.json`,
|
|
221
|
+
JSON.stringify(metadata)
|
|
222
|
+
);
|
|
223
|
+
const checked = await checkCompiledContext(
|
|
224
|
+
{ capsule, markdown: preview.markdown },
|
|
225
|
+
deps
|
|
226
|
+
);
|
|
227
|
+
if (
|
|
228
|
+
checked.status !== "current" ||
|
|
229
|
+
(await readBounded(capsulePath)) !== capsuleText
|
|
230
|
+
)
|
|
231
|
+
throw new Error("Source or policy changed before publication");
|
|
232
|
+
await absent(outputPath);
|
|
233
|
+
await absent(`${outputPath}.json`);
|
|
234
|
+
await link(stagedSidecar, `${outputPath}.json`);
|
|
235
|
+
publishedSidecar = true;
|
|
236
|
+
await link(stagedOutput, outputPath);
|
|
237
|
+
return result(outputPath, capsulePath, preview, "written");
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (publishedSidecar) await removeTemp(`${outputPath}.json`);
|
|
240
|
+
throw error;
|
|
241
|
+
} finally {
|
|
242
|
+
await removeTemp(stagedOutput);
|
|
243
|
+
if (stagedSidecar) await removeTemp(stagedSidecar);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function checkContextFile(
|
|
249
|
+
input: { outputPath: string; capsulePath?: string },
|
|
250
|
+
deps: CompiledContextRuntimeDeps
|
|
251
|
+
): Promise<CompiledContextCheck> {
|
|
252
|
+
try {
|
|
253
|
+
const outputPath = outputTarget(input.outputPath);
|
|
254
|
+
const saved = await owned(outputPath);
|
|
255
|
+
const capsule = JSON.parse(
|
|
256
|
+
await readBounded(input.capsulePath ?? saved.snapshot.capsulePath)
|
|
257
|
+
);
|
|
258
|
+
return await checkCompiledContext(
|
|
259
|
+
{ capsule, markdown: saved.markdown },
|
|
260
|
+
deps
|
|
261
|
+
);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
const message =
|
|
264
|
+
error instanceof Error ? error.message : "Unavailable compiled context";
|
|
265
|
+
return {
|
|
266
|
+
schemaVersion: "1.0",
|
|
267
|
+
status: message.startsWith("Conflict:") ? "conflict" : "unverifiable",
|
|
268
|
+
reasons: [message],
|
|
269
|
+
digest: null,
|
|
270
|
+
capsuleId: null,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function refreshContextFile(
|
|
276
|
+
input: { outputPath: string; capsuleOutputPath: string },
|
|
277
|
+
deps: CompiledContextRuntimeDeps,
|
|
278
|
+
build: (request: ContextCapsuleBuildInput) => Promise<ContextCapsuleV1>
|
|
279
|
+
): Promise<CompiledContextFileResult> {
|
|
280
|
+
const outputPath = outputTarget(input.outputPath);
|
|
281
|
+
if (!input.capsuleOutputPath.endsWith(".gno-context.capsule.json"))
|
|
282
|
+
throw new Error("Refreshed Capsule must end in .gno-context.capsule.json");
|
|
283
|
+
const capsulePath = await safePath(input.capsuleOutputPath, true);
|
|
284
|
+
await safePath(`${outputPath}.lock`, true);
|
|
285
|
+
return withWriteLock(`${outputPath}.lock`, async () => {
|
|
286
|
+
const saved = await owned(outputPath);
|
|
287
|
+
const priorCapsuleText = await readBounded(saved.snapshot.capsulePath);
|
|
288
|
+
const priorCapsule = JSON.parse(priorCapsuleText);
|
|
289
|
+
const current = await checkCompiledContext(
|
|
290
|
+
{ capsule: priorCapsule, markdown: saved.markdown },
|
|
291
|
+
deps
|
|
292
|
+
);
|
|
293
|
+
if (current.status === "current") {
|
|
294
|
+
const preview = await previewCompiledContext(
|
|
295
|
+
{ capsule: priorCapsule, ...saved.snapshot.settings },
|
|
296
|
+
deps
|
|
297
|
+
);
|
|
298
|
+
await unchanged(outputPath, saved);
|
|
299
|
+
if ((await readBounded(saved.snapshot.capsulePath)) !== priorCapsuleText)
|
|
300
|
+
throw new Error("Capsule changed during refresh");
|
|
301
|
+
return result(
|
|
302
|
+
outputPath,
|
|
303
|
+
saved.snapshot.capsulePath,
|
|
304
|
+
preview,
|
|
305
|
+
"unchanged"
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
if (current.status !== "stale")
|
|
309
|
+
throw new Error(
|
|
310
|
+
`Cannot refresh ${current.status} context: ${current.reasons.join("; ")}`
|
|
311
|
+
);
|
|
312
|
+
await absent(capsulePath);
|
|
313
|
+
// Stage first so an unwritable Capsule destination fails before retrieval/publication.
|
|
314
|
+
const stagedCapsule = await stage(capsulePath, "");
|
|
315
|
+
let stagedOutput: string | undefined;
|
|
316
|
+
let stagedSidecar: string | undefined;
|
|
317
|
+
try {
|
|
318
|
+
const capsule = await build(
|
|
319
|
+
compiledContextRefreshRequest(priorCapsule, deps)
|
|
320
|
+
);
|
|
321
|
+
const preview = await previewCompiledContext(
|
|
322
|
+
{ capsule, ...saved.snapshot.settings },
|
|
323
|
+
deps
|
|
324
|
+
);
|
|
325
|
+
await Bun.write(stagedCapsule, JSON.stringify(capsule));
|
|
326
|
+
stagedOutput = await stage(outputPath, preview.markdown);
|
|
327
|
+
stagedSidecar = await stage(
|
|
328
|
+
`${outputPath}.json`,
|
|
329
|
+
JSON.stringify(
|
|
330
|
+
sidecar(
|
|
331
|
+
{
|
|
332
|
+
capsulePath,
|
|
333
|
+
outputDigest: preview.digest,
|
|
334
|
+
settings: saved.snapshot.settings,
|
|
335
|
+
},
|
|
336
|
+
saved.snapshot
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
);
|
|
340
|
+
const checked = await checkCompiledContext(
|
|
341
|
+
{ capsule, markdown: preview.markdown },
|
|
342
|
+
deps
|
|
343
|
+
);
|
|
344
|
+
if (checked.status !== "current")
|
|
345
|
+
throw new Error("Source or policy changed before publication");
|
|
346
|
+
await unchanged(outputPath, saved);
|
|
347
|
+
if ((await readBounded(saved.snapshot.capsulePath)) !== priorCapsuleText)
|
|
348
|
+
throw new Error("Capsule changed during refresh");
|
|
349
|
+
await absent(capsulePath);
|
|
350
|
+
await link(stagedCapsule, capsulePath);
|
|
351
|
+
// Retain one prior reference: either output remains verifiable after interruption.
|
|
352
|
+
await rename(stagedSidecar, `${outputPath}.json`);
|
|
353
|
+
await rename(stagedOutput, outputPath);
|
|
354
|
+
return result(outputPath, capsulePath, preview, "written");
|
|
355
|
+
} finally {
|
|
356
|
+
await removeTemp(stagedCapsule);
|
|
357
|
+
if (stagedOutput) await removeTemp(stagedOutput);
|
|
358
|
+
if (stagedSidecar) await removeTemp(stagedSidecar);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/** Verification and caller authority shared by all compiled-context surfaces. */
|
|
2
|
+
import type { ContextCapsuleV1 } from "../core/context-capsule";
|
|
3
|
+
import type {
|
|
4
|
+
EgressCallerContext,
|
|
5
|
+
EgressDestinationZone,
|
|
6
|
+
} from "../core/egress-policy";
|
|
7
|
+
import type {
|
|
8
|
+
ContextCapsuleBuildInput,
|
|
9
|
+
ContextCapsuleRuntimeDeps,
|
|
10
|
+
} from "./context-runtime";
|
|
11
|
+
|
|
12
|
+
import { currentEgressSources } from "../core/collection-egress-policy-service";
|
|
13
|
+
import {
|
|
14
|
+
compiledContextPreviewInputSchema,
|
|
15
|
+
compiledContextCheckInputSchema,
|
|
16
|
+
COMPILED_CONTEXT_MAX_BYTES,
|
|
17
|
+
readCompiledContextMetadata,
|
|
18
|
+
compiledContextBodyMatches,
|
|
19
|
+
renderCompiledContext,
|
|
20
|
+
type CompiledContextCheck,
|
|
21
|
+
type CompiledContextPreview,
|
|
22
|
+
} from "../core/compiled-context";
|
|
23
|
+
import { sha256Text } from "../core/context-capsule-validation";
|
|
24
|
+
import {
|
|
25
|
+
parseCanonicalContextCapsuleForVerification,
|
|
26
|
+
rawCanonicalContextJson,
|
|
27
|
+
} from "../core/context-verifier-input";
|
|
28
|
+
import { evaluateEgressPolicy } from "../core/egress-policy";
|
|
29
|
+
import { resolveEgressLineage } from "../core/egress-provenance";
|
|
30
|
+
import {
|
|
31
|
+
verifyContextCapsuleRuntime,
|
|
32
|
+
canonicalVerifiedContextCapsuleJson,
|
|
33
|
+
} from "./context-runtime";
|
|
34
|
+
export interface CompiledContextRuntimeDeps extends ContextCapsuleRuntimeDeps {
|
|
35
|
+
destinationZone?: EgressDestinationZone;
|
|
36
|
+
caller?: EgressCallerContext;
|
|
37
|
+
}
|
|
38
|
+
class CompiledContextStateError extends Error {
|
|
39
|
+
constructor(
|
|
40
|
+
readonly state: "stale" | "unverifiable",
|
|
41
|
+
message: string
|
|
42
|
+
) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "CompiledContextStateError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function parse(
|
|
48
|
+
input: unknown,
|
|
49
|
+
deps: CompiledContextRuntimeDeps
|
|
50
|
+
): ContextCapsuleV1 {
|
|
51
|
+
if (
|
|
52
|
+
new TextEncoder().encode(rawCanonicalContextJson(input)).byteLength >
|
|
53
|
+
COMPILED_CONTEXT_MAX_BYTES
|
|
54
|
+
)
|
|
55
|
+
throw new Error("Capsule exceeds 4 MiB");
|
|
56
|
+
const capsule = parseCanonicalContextCapsuleForVerification(input, deps);
|
|
57
|
+
if (capsule.schemaVersion === "1.0")
|
|
58
|
+
throw new Error(
|
|
59
|
+
"Rebuild legacy Capsule with current provenance before compiling"
|
|
60
|
+
);
|
|
61
|
+
return capsule;
|
|
62
|
+
}
|
|
63
|
+
function authorize(
|
|
64
|
+
capsule: ContextCapsuleV1,
|
|
65
|
+
deps: CompiledContextRuntimeDeps
|
|
66
|
+
): string {
|
|
67
|
+
if (capsule.schemaVersion === "1.0")
|
|
68
|
+
throw new Error("Capsule lineage unavailable");
|
|
69
|
+
const names = [
|
|
70
|
+
...new Set([
|
|
71
|
+
...capsule.scope.collections,
|
|
72
|
+
...capsule.evidence.map((item) => item.collection),
|
|
73
|
+
...capsule.egressLineage.sources.map((item) => item.collection),
|
|
74
|
+
]),
|
|
75
|
+
];
|
|
76
|
+
if (
|
|
77
|
+
names.some(
|
|
78
|
+
(name) =>
|
|
79
|
+
!deps.config.collections.some((collection) => collection.name === name)
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
throw new CompiledContextStateError(
|
|
83
|
+
"unverifiable",
|
|
84
|
+
"Collection scope is no longer configured; rebuild within eligible scope"
|
|
85
|
+
);
|
|
86
|
+
const sources = currentEgressSources(deps.config, names);
|
|
87
|
+
const decision = evaluateEgressPolicy({
|
|
88
|
+
collections: sources,
|
|
89
|
+
action: "export",
|
|
90
|
+
destination: { zone: deps.destinationZone ?? "local_process" },
|
|
91
|
+
caller: deps.caller ?? { authenticated: true, operationAuthorized: true },
|
|
92
|
+
contentClass: "capsule",
|
|
93
|
+
});
|
|
94
|
+
if (!decision.allowed)
|
|
95
|
+
throw new CompiledContextStateError(
|
|
96
|
+
"unverifiable",
|
|
97
|
+
"Current collection policy or caller authority denies this context"
|
|
98
|
+
);
|
|
99
|
+
const lineage = resolveEgressLineage(sources, names);
|
|
100
|
+
if (lineage.digest !== capsule.egressLineage.digest)
|
|
101
|
+
throw new CompiledContextStateError(
|
|
102
|
+
"stale",
|
|
103
|
+
"Collection policy changed; rebuild and verify the Capsule"
|
|
104
|
+
);
|
|
105
|
+
return lineage.digest;
|
|
106
|
+
}
|
|
107
|
+
async function verified(
|
|
108
|
+
capsule: ContextCapsuleV1,
|
|
109
|
+
deps: CompiledContextRuntimeDeps
|
|
110
|
+
): Promise<{ verificationDigest: string; lineageDigest: string }> {
|
|
111
|
+
const lineageDigest = authorize(capsule, deps);
|
|
112
|
+
const receipt = await verifyContextCapsuleRuntime(capsule, deps);
|
|
113
|
+
if (
|
|
114
|
+
receipt.contentStatus !== "unchanged" ||
|
|
115
|
+
receipt.fingerprintStatus !== "unchanged"
|
|
116
|
+
)
|
|
117
|
+
throw new CompiledContextStateError(
|
|
118
|
+
"stale",
|
|
119
|
+
`Capsule requires refresh: ${[receipt.contentCode, ...receipt.fingerprintReasons].join(", ")}`
|
|
120
|
+
);
|
|
121
|
+
authorize(capsule, deps);
|
|
122
|
+
return {
|
|
123
|
+
verificationDigest: sha256Text(
|
|
124
|
+
canonicalVerifiedContextCapsuleJson(receipt)
|
|
125
|
+
),
|
|
126
|
+
lineageDigest,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export async function previewCompiledContext(
|
|
130
|
+
input: unknown,
|
|
131
|
+
deps: CompiledContextRuntimeDeps
|
|
132
|
+
): Promise<CompiledContextPreview> {
|
|
133
|
+
const settings = compiledContextPreviewInputSchema.parse(input);
|
|
134
|
+
const capsule = parse(settings.capsule, deps);
|
|
135
|
+
const identity = await verified(capsule, deps);
|
|
136
|
+
const preview = renderCompiledContext(
|
|
137
|
+
capsule,
|
|
138
|
+
settings,
|
|
139
|
+
identity.verificationDigest,
|
|
140
|
+
identity.lineageDigest,
|
|
141
|
+
deps.countTokens
|
|
142
|
+
);
|
|
143
|
+
const finalIdentity = await verified(capsule, deps);
|
|
144
|
+
if (finalIdentity.verificationDigest !== identity.verificationDigest)
|
|
145
|
+
throw new CompiledContextStateError(
|
|
146
|
+
"stale",
|
|
147
|
+
"Index changed during compilation; retry after indexing completes"
|
|
148
|
+
);
|
|
149
|
+
return preview;
|
|
150
|
+
}
|
|
151
|
+
export async function checkCompiledContext(
|
|
152
|
+
input: unknown,
|
|
153
|
+
deps: CompiledContextRuntimeDeps
|
|
154
|
+
): Promise<CompiledContextCheck> {
|
|
155
|
+
const result = (
|
|
156
|
+
status: CompiledContextCheck["status"],
|
|
157
|
+
reasons: string[],
|
|
158
|
+
digest: string | null = null,
|
|
159
|
+
capsuleId: string | null = null
|
|
160
|
+
): CompiledContextCheck => ({
|
|
161
|
+
schemaVersion: "1.0",
|
|
162
|
+
status,
|
|
163
|
+
reasons,
|
|
164
|
+
digest,
|
|
165
|
+
capsuleId,
|
|
166
|
+
});
|
|
167
|
+
try {
|
|
168
|
+
const { capsule: raw, markdown } =
|
|
169
|
+
compiledContextCheckInputSchema.parse(input);
|
|
170
|
+
const capsule = parse(raw, deps);
|
|
171
|
+
// Authorize before reporting identifiers from supplied private evidence.
|
|
172
|
+
authorize(capsule, deps);
|
|
173
|
+
const meta = readCompiledContextMetadata(markdown);
|
|
174
|
+
if (!compiledContextBodyMatches(markdown))
|
|
175
|
+
return result("conflict", [
|
|
176
|
+
"Output bytes were edited; preserve edits before recompiling",
|
|
177
|
+
]);
|
|
178
|
+
const expected = await previewCompiledContext(
|
|
179
|
+
{
|
|
180
|
+
capsule,
|
|
181
|
+
budgetTokens: meta.budgetTokens,
|
|
182
|
+
budgetBytes: meta.budgetBytes,
|
|
183
|
+
},
|
|
184
|
+
deps
|
|
185
|
+
);
|
|
186
|
+
if (expected.markdown !== markdown)
|
|
187
|
+
return result("conflict", [
|
|
188
|
+
"Artifact metadata, renderer settings, or Capsule identity differs",
|
|
189
|
+
]);
|
|
190
|
+
return result("current", [], expected.digest, capsule.capsuleId);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (error instanceof CompiledContextStateError)
|
|
193
|
+
return result(error.state, [error.message]);
|
|
194
|
+
// Do not echo parser paths/values or supplied private source material.
|
|
195
|
+
return result("unverifiable", [
|
|
196
|
+
"Invalid or unavailable Capsule, artifact metadata, index, or recorded tokenizer; rebuild or supply the original matching inputs",
|
|
197
|
+
]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
export function compiledContextRefreshRequest(
|
|
201
|
+
input: unknown,
|
|
202
|
+
authority: Pick<
|
|
203
|
+
CompiledContextRuntimeDeps,
|
|
204
|
+
"countTokens" | "tokenizerFingerprint"
|
|
205
|
+
> = {}
|
|
206
|
+
): ContextCapsuleBuildInput {
|
|
207
|
+
// Refresh cannot trust stale fingerprints, but must preserve the saved request exactly.
|
|
208
|
+
const capsule = parseCanonicalContextCapsuleForVerification(input, authority);
|
|
209
|
+
if (capsule.schemaVersion === "1.0")
|
|
210
|
+
throw new Error("Rebuild legacy Capsule before refresh");
|
|
211
|
+
const request = capsule.retrieval.request;
|
|
212
|
+
return {
|
|
213
|
+
goal: capsule.goal,
|
|
214
|
+
query: capsule.query,
|
|
215
|
+
indexName: capsule.scope.indexName,
|
|
216
|
+
collections: capsule.scope.collections,
|
|
217
|
+
uriPrefix: capsule.scope.uriPrefix,
|
|
218
|
+
tagsAll: capsule.scope.tagsAll,
|
|
219
|
+
tagsAny: capsule.scope.tagsAny,
|
|
220
|
+
categories: capsule.scope.categories,
|
|
221
|
+
since: capsule.scope.since ?? undefined,
|
|
222
|
+
until: capsule.scope.until ?? undefined,
|
|
223
|
+
filter: "filter" in capsule.scope ? capsule.scope.filter : undefined,
|
|
224
|
+
author: request.author ?? undefined,
|
|
225
|
+
lang: request.lang ?? undefined,
|
|
226
|
+
intent: request.intent ?? undefined,
|
|
227
|
+
exclude: request.exclude,
|
|
228
|
+
minScore: request.minScore ?? undefined,
|
|
229
|
+
queryModes: request.queryModes,
|
|
230
|
+
limit: request.limit,
|
|
231
|
+
candidateLimit: request.candidateLimit,
|
|
232
|
+
graph: request.graphRequested,
|
|
233
|
+
noRerank: request.rerankRequested === false,
|
|
234
|
+
depthPolicy: capsule.retrieval.depthPolicy,
|
|
235
|
+
budgetTokens: capsule.budget.requestedTokens,
|
|
236
|
+
budgetBytes: capsule.budget.requestedBytes,
|
|
237
|
+
safetyMarginTokens: capsule.budget.safetyMarginTokens,
|
|
238
|
+
safetyMarginBytes: capsule.budget.safetyMarginBytes,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -154,7 +154,8 @@ export const projectContextCapsule = (
|
|
|
154
154
|
egressLineage: ContextCapsulePayloadV1_1["egressLineage"];
|
|
155
155
|
},
|
|
156
156
|
input: NormalizedContextBuildInput,
|
|
157
|
-
deps: ContextCapsuleRuntimeDeps
|
|
157
|
+
deps: ContextCapsuleRuntimeDeps,
|
|
158
|
+
metadataCoverageIncomplete = false
|
|
158
159
|
): ContextCanonicalProjection<ContextCapsuleV1> | null => {
|
|
159
160
|
if (draft.selection.selected.length === 0) return null;
|
|
160
161
|
const evidence = draft.selection.selected.map((candidate, index) =>
|
|
@@ -286,7 +287,37 @@ export const projectContextCapsule = (
|
|
|
286
287
|
],
|
|
287
288
|
};
|
|
288
289
|
try {
|
|
289
|
-
const
|
|
290
|
+
const versionedPayload =
|
|
291
|
+
input.filter === undefined
|
|
292
|
+
? payload
|
|
293
|
+
: {
|
|
294
|
+
...payload,
|
|
295
|
+
schemaVersion: "1.2" as const,
|
|
296
|
+
coverage: {
|
|
297
|
+
...payload.coverage,
|
|
298
|
+
complete:
|
|
299
|
+
payload.coverage.complete && !metadataCoverageIncomplete,
|
|
300
|
+
},
|
|
301
|
+
warnings: [
|
|
302
|
+
...payload.warnings,
|
|
303
|
+
...(metadataCoverageIncomplete && payload.coverage.complete
|
|
304
|
+
? [{ code: "incomplete_coverage" as const }]
|
|
305
|
+
: []),
|
|
306
|
+
...(metadataCoverageIncomplete
|
|
307
|
+
? [{ code: "metadata_coverage_incomplete" as const }]
|
|
308
|
+
: []),
|
|
309
|
+
],
|
|
310
|
+
scope: { ...payload.scope, filter: input.filter },
|
|
311
|
+
retrieval: {
|
|
312
|
+
...payload.retrieval,
|
|
313
|
+
request: { ...payload.retrieval.request, filter: input.filter },
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
versionedPayload.fingerprints.retrieval = retrievalFingerprint(
|
|
317
|
+
versionedPayload,
|
|
318
|
+
snapshots.contextFingerprint
|
|
319
|
+
);
|
|
320
|
+
const value = createContextCapsuleV1(versionedPayload, {
|
|
290
321
|
countTokens: deps.countTokens,
|
|
291
322
|
});
|
|
292
323
|
return {
|
|
@@ -5,6 +5,10 @@ import type { ContextCapsuleBuildInput } from "./context-runtime-types";
|
|
|
5
5
|
|
|
6
6
|
import { isValidLanguageHint } from "../config/types";
|
|
7
7
|
import { normalizeTag, validateTag } from "../core/tags";
|
|
8
|
+
import {
|
|
9
|
+
metadataPredicateSchema,
|
|
10
|
+
normalizeMetadataPredicate,
|
|
11
|
+
} from "../core/typed-metadata";
|
|
8
12
|
import { resolveTemporalRange } from "../pipeline/temporal";
|
|
9
13
|
import { buildUri, parseUri } from "./constants";
|
|
10
14
|
import { ContextRuntimeError } from "./context-runtime-types";
|
|
@@ -355,8 +359,21 @@ export const normalizeContextBuildInput = (
|
|
|
355
359
|
"Context author or language filter is invalid"
|
|
356
360
|
);
|
|
357
361
|
}
|
|
362
|
+
const parsedFilter =
|
|
363
|
+
input.filter === undefined
|
|
364
|
+
? undefined
|
|
365
|
+
: metadataPredicateSchema.safeParse(input.filter);
|
|
366
|
+
if (parsedFilter && !parsedFilter.success) {
|
|
367
|
+
throw new ContextRuntimeError(
|
|
368
|
+
"invalid_filter",
|
|
369
|
+
`filter: ${parsedFilter.error.message}`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
358
372
|
return {
|
|
359
373
|
...input,
|
|
374
|
+
...(parsedFilter?.success
|
|
375
|
+
? { filter: normalizeMetadataPredicate(parsedFilter.data) }
|
|
376
|
+
: {}),
|
|
360
377
|
goal,
|
|
361
378
|
query,
|
|
362
379
|
indexName,
|
|
@@ -3,6 +3,7 @@ import type { ContextCapsuleV1 } from "../core/context-capsule";
|
|
|
3
3
|
import type { ContextEvidenceCompilerDeps } from "../core/context-evidence";
|
|
4
4
|
import type { ContextVerifierDeps } from "../core/context-verifier";
|
|
5
5
|
import type { RetrievalTraceSession } from "../core/retrieval-trace-session";
|
|
6
|
+
import type { MetadataPredicate } from "../core/typed-metadata";
|
|
6
7
|
import type { EmbeddingPort, RerankPort } from "../llm/types";
|
|
7
8
|
import type { RequestHydration } from "../pipeline/hydration";
|
|
8
9
|
import type { ProjectAffinityScoringInput } from "../pipeline/project-affinity";
|
|
@@ -22,6 +23,7 @@ export interface ContextCapsuleBuildInput {
|
|
|
22
23
|
tagsAll?: string[];
|
|
23
24
|
tagsAny?: string[];
|
|
24
25
|
categories?: string[];
|
|
26
|
+
filter?: MetadataPredicate;
|
|
25
27
|
author?: string;
|
|
26
28
|
lang?: string;
|
|
27
29
|
intent?: string;
|