@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.
- package/README.md +20 -16
- package/assets/skill/SKILL.md +8 -5
- package/package.json +1 -1
- package/src/app/context-agent-projection.ts +303 -0
- package/src/app/context-format.ts +249 -0
- package/src/app/context-runtime-contract.ts +325 -0
- package/src/app/context-runtime-input.ts +362 -0
- package/src/app/context-runtime-types.ts +65 -0
- package/src/app/context-runtime.ts +170 -0
- package/src/app/context-surface.ts +145 -0
- package/src/cli/commands/context-build.ts +149 -0
- package/src/cli/commands/context-verify.ts +90 -0
- package/src/cli/options.ts +4 -0
- package/src/cli/program.ts +178 -0
- package/src/core/context-budget.ts +461 -0
- package/src/core/context-capsule-index-schema.ts +15 -0
- package/src/core/context-capsule-retrieval-schema.ts +81 -0
- package/src/core/context-capsule-schema.ts +473 -0
- package/src/core/context-capsule-validation.ts +416 -0
- package/src/core/context-capsule-verification.ts +218 -0
- package/src/core/context-capsule.ts +439 -0
- package/src/core/context-compiler.ts +513 -0
- package/src/core/context-evidence-metadata.ts +33 -0
- package/src/core/context-evidence.ts +495 -0
- package/src/core/context-facets.ts +163 -0
- package/src/core/context-guidance.ts +69 -0
- package/src/core/context-scope.ts +32 -0
- package/src/core/context-verifier-canonical.ts +90 -0
- package/src/core/context-verifier-input.ts +66 -0
- package/src/core/context-verifier.ts +447 -0
- package/src/core/sections.ts +63 -0
- package/src/mcp/server.ts +10 -4
- package/src/mcp/tools/context.ts +229 -0
- package/src/mcp/tools/index.ts +27 -0
- package/src/pipeline/chunk-lookup.ts +33 -0
- package/src/pipeline/hybrid.ts +79 -57
- package/src/pipeline/types.ts +14 -0
- package/src/sdk/client.ts +68 -6
- package/src/sdk/index.ts +21 -0
- package/src/sdk/types.ts +24 -0
- package/src/serve/background-runtime.ts +1 -0
- package/src/serve/context-capsule.ts +136 -0
- package/src/serve/context.ts +10 -1
- package/src/serve/routes/api.ts +2 -0
- package/src/serve/server.ts +23 -0
- package/src/store/sqlite/adapter.ts +38 -20
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { SearchResult } from "../pipeline/types";
|
|
2
|
+
import type { ContextRow } from "../store/types";
|
|
3
|
+
|
|
4
|
+
import { decorateUriForIndex } from "../app/constants";
|
|
5
|
+
import { contextCapsuleContextIdentity } from "./context-capsule-validation";
|
|
6
|
+
import {
|
|
7
|
+
contextIdentityFromUri,
|
|
8
|
+
resolveContextSnapshot,
|
|
9
|
+
} from "./context-resolver";
|
|
10
|
+
|
|
11
|
+
export interface ContextConfiguredGuidance {
|
|
12
|
+
contextId: string;
|
|
13
|
+
scopeType: "global" | "collection" | "prefix";
|
|
14
|
+
scopeKey: string;
|
|
15
|
+
text: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const compareCodeUnits = (left: string, right: string): number => {
|
|
19
|
+
if (left < right) return -1;
|
|
20
|
+
if (left > right) return 1;
|
|
21
|
+
return 0;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** A docid is only a short source-hash prefix and cannot identify a document. */
|
|
25
|
+
export const contextGuidanceResultIdentity = (
|
|
26
|
+
result: Pick<SearchResult, "docid" | "uri">
|
|
27
|
+
): string => JSON.stringify([result.uri, result.docid]);
|
|
28
|
+
|
|
29
|
+
export const resolveContextGuidance = (
|
|
30
|
+
contextSnapshot: ContextRow[],
|
|
31
|
+
results: SearchResult[],
|
|
32
|
+
indexName: string
|
|
33
|
+
): {
|
|
34
|
+
contexts: ContextConfiguredGuidance[];
|
|
35
|
+
idsByResultIdentity: Map<string, string[]>;
|
|
36
|
+
} => {
|
|
37
|
+
const byId = new Map<string, ContextConfiguredGuidance>();
|
|
38
|
+
const idsByResultIdentity = new Map<string, string[]>();
|
|
39
|
+
for (const result of results) {
|
|
40
|
+
const ids: string[] = [];
|
|
41
|
+
const identity = contextIdentityFromUri(result.uri);
|
|
42
|
+
const resolved = identity
|
|
43
|
+
? resolveContextSnapshot(contextSnapshot, identity)
|
|
44
|
+
: undefined;
|
|
45
|
+
for (const provenance of resolved?.provenance ?? []) {
|
|
46
|
+
const guidance = {
|
|
47
|
+
scopeType: provenance.scopeType,
|
|
48
|
+
scopeKey:
|
|
49
|
+
provenance.scopeType === "prefix"
|
|
50
|
+
? decorateUriForIndex(provenance.normalizedScopeKey, indexName)
|
|
51
|
+
: provenance.normalizedScopeKey,
|
|
52
|
+
text: provenance.text,
|
|
53
|
+
};
|
|
54
|
+
const contextId = contextCapsuleContextIdentity(guidance);
|
|
55
|
+
byId.set(contextId, { contextId, ...guidance });
|
|
56
|
+
ids.push(contextId);
|
|
57
|
+
}
|
|
58
|
+
idsByResultIdentity.set(
|
|
59
|
+
contextGuidanceResultIdentity(result),
|
|
60
|
+
[...new Set(ids)].sort(compareCodeUnits)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
contexts: [...byId.values()].sort((left, right) =>
|
|
65
|
+
compareCodeUnits(left.contextId, right.contextId)
|
|
66
|
+
),
|
|
67
|
+
idsByResultIdentity,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Canonical URI scope matching shared by Context planning stages. */
|
|
2
|
+
|
|
3
|
+
import { DEFAULT_INDEX_NAME, parseUri } from "../app/constants";
|
|
4
|
+
import { canonicalizeIndexName } from "../app/index-name";
|
|
5
|
+
|
|
6
|
+
export const isContextUriInScope = (
|
|
7
|
+
uri: string,
|
|
8
|
+
indexName: string,
|
|
9
|
+
collections: string[],
|
|
10
|
+
prefixValue: string | null
|
|
11
|
+
): boolean => {
|
|
12
|
+
const value = parseUri(uri);
|
|
13
|
+
if (
|
|
14
|
+
!value ||
|
|
15
|
+
canonicalizeIndexName(value.indexName ?? DEFAULT_INDEX_NAME) !==
|
|
16
|
+
indexName ||
|
|
17
|
+
(collections.length > 0 && !collections.includes(value.collection))
|
|
18
|
+
) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (prefixValue === null) return true;
|
|
22
|
+
const prefix = parseUri(prefixValue);
|
|
23
|
+
return Boolean(
|
|
24
|
+
prefix &&
|
|
25
|
+
value.collection === prefix.collection &&
|
|
26
|
+
canonicalizeIndexName(prefix.indexName ?? DEFAULT_INDEX_NAME) ===
|
|
27
|
+
indexName &&
|
|
28
|
+
(prefix.path === "" ||
|
|
29
|
+
value.path === prefix.path ||
|
|
30
|
+
value.path.startsWith(`${prefix.path}/`))
|
|
31
|
+
);
|
|
32
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/** Stable JSON and raw text checks used by Capsule verification guards. */
|
|
2
|
+
|
|
3
|
+
const compareCodeUnits = (left: string, right: string): number =>
|
|
4
|
+
left < right ? -1 : left > right ? 1 : 0;
|
|
5
|
+
|
|
6
|
+
const canonicalizeJsonValue = (value: unknown): unknown => {
|
|
7
|
+
if (Array.isArray(value)) return value.map(canonicalizeJsonValue);
|
|
8
|
+
if (value !== null && typeof value === "object") {
|
|
9
|
+
const sorted: Record<string, unknown> = {};
|
|
10
|
+
for (const key of Object.keys(value).sort(compareCodeUnits)) {
|
|
11
|
+
const child = (value as Record<string, unknown>)[key];
|
|
12
|
+
if (child === undefined) {
|
|
13
|
+
throw new Error(`Canonical JSON rejects undefined at ${key}`);
|
|
14
|
+
}
|
|
15
|
+
sorted[key] = canonicalizeJsonValue(child);
|
|
16
|
+
}
|
|
17
|
+
return sorted;
|
|
18
|
+
}
|
|
19
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
20
|
+
throw new Error("Canonical JSON rejects non-finite numbers");
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const canonicalVerifierJson = (value: unknown): string =>
|
|
26
|
+
JSON.stringify(canonicalizeJsonValue(value));
|
|
27
|
+
|
|
28
|
+
const isNoncanonicalNormalizedText = (value: unknown): boolean => {
|
|
29
|
+
if (typeof value === "string") {
|
|
30
|
+
return value.includes("\r") || value !== value.normalize("NFC");
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const containsNoncanonicalText = (value: unknown): boolean =>
|
|
36
|
+
Array.isArray(value) && value.some(isNoncanonicalNormalizedText);
|
|
37
|
+
|
|
38
|
+
const recordOf = (value: unknown): Record<string, unknown> =>
|
|
39
|
+
value !== null && typeof value === "object"
|
|
40
|
+
? (value as Record<string, unknown>)
|
|
41
|
+
: {};
|
|
42
|
+
|
|
43
|
+
/** Mirrors only the text fields normalized by normalizePayload. */
|
|
44
|
+
export const hasNoncanonicalVerifierText = (value: unknown): boolean => {
|
|
45
|
+
const capsule = recordOf(value);
|
|
46
|
+
const scope = recordOf(capsule.scope);
|
|
47
|
+
const retrieval = recordOf(capsule.retrieval);
|
|
48
|
+
const guidance = recordOf(capsule.guidance);
|
|
49
|
+
const coverage = recordOf(capsule.coverage);
|
|
50
|
+
const evidence = Array.isArray(capsule.evidence) ? capsule.evidence : [];
|
|
51
|
+
const configuredContexts = Array.isArray(guidance.configuredContexts)
|
|
52
|
+
? guidance.configuredContexts
|
|
53
|
+
: [];
|
|
54
|
+
const coveredFacets = Array.isArray(coverage.coveredFacets)
|
|
55
|
+
? coverage.coveredFacets
|
|
56
|
+
: [];
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
isNoncanonicalNormalizedText(capsule.goal) ||
|
|
60
|
+
isNoncanonicalNormalizedText(capsule.query) ||
|
|
61
|
+
isNoncanonicalNormalizedText(scope.uriPrefix) ||
|
|
62
|
+
containsNoncanonicalText(scope.collections) ||
|
|
63
|
+
containsNoncanonicalText(scope.tagsAll) ||
|
|
64
|
+
containsNoncanonicalText(scope.tagsAny) ||
|
|
65
|
+
containsNoncanonicalText(scope.categories) ||
|
|
66
|
+
containsNoncanonicalText(retrieval.facets) ||
|
|
67
|
+
containsNoncanonicalText(retrieval.queryVariants) ||
|
|
68
|
+
evidence.some((item) => {
|
|
69
|
+
const record = recordOf(item);
|
|
70
|
+
return (
|
|
71
|
+
isNoncanonicalNormalizedText(record.title) ||
|
|
72
|
+
isNoncanonicalNormalizedText(record.heading) ||
|
|
73
|
+
containsNoncanonicalText(record.contextIds) ||
|
|
74
|
+
containsNoncanonicalText(record.facets)
|
|
75
|
+
);
|
|
76
|
+
}) ||
|
|
77
|
+
configuredContexts.some((item) => {
|
|
78
|
+
const record = recordOf(item);
|
|
79
|
+
return (
|
|
80
|
+
isNoncanonicalNormalizedText(record.scopeKey) ||
|
|
81
|
+
isNoncanonicalNormalizedText(record.text)
|
|
82
|
+
);
|
|
83
|
+
}) ||
|
|
84
|
+
containsNoncanonicalText(coverage.requestedFacets) ||
|
|
85
|
+
coveredFacets.some((item) =>
|
|
86
|
+
isNoncanonicalNormalizedText(recordOf(item).facet)
|
|
87
|
+
) ||
|
|
88
|
+
containsNoncanonicalText(coverage.unresolvedFacets)
|
|
89
|
+
);
|
|
90
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Canonical verification input preflight; performs no store I/O. */
|
|
2
|
+
|
|
3
|
+
import type { ContextCapsuleV1 } from "./context-capsule";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
canonicalContextCapsuleJson,
|
|
7
|
+
ContextCapsuleContractError,
|
|
8
|
+
parseContextCapsuleV1,
|
|
9
|
+
type ContextCapsuleCreateOptions,
|
|
10
|
+
} from "./context-capsule";
|
|
11
|
+
import {
|
|
12
|
+
canonicalVerifierJson,
|
|
13
|
+
hasNoncanonicalVerifierText,
|
|
14
|
+
} from "./context-verifier-canonical";
|
|
15
|
+
|
|
16
|
+
export interface ContextVerifierTokenAuthority {
|
|
17
|
+
countTokens?: ContextCapsuleCreateOptions["countTokens"];
|
|
18
|
+
tokenizerFingerprint?: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const rawCanonicalContextJson = (input: unknown): string => {
|
|
22
|
+
try {
|
|
23
|
+
return canonicalVerifierJson(input);
|
|
24
|
+
} catch (cause) {
|
|
25
|
+
throw new ContextCapsuleContractError(
|
|
26
|
+
"invalid_input",
|
|
27
|
+
"Context Capsule input must be canonical JSON",
|
|
28
|
+
{ cause }
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const parseCanonicalContextCapsuleForVerification = (
|
|
34
|
+
input: unknown,
|
|
35
|
+
authority: ContextVerifierTokenAuthority = {}
|
|
36
|
+
): ContextCapsuleV1 => {
|
|
37
|
+
const rawInputBefore = rawCanonicalContextJson(input);
|
|
38
|
+
if (hasNoncanonicalVerifierText(input)) {
|
|
39
|
+
throw new ContextCapsuleContractError(
|
|
40
|
+
"invalid_input",
|
|
41
|
+
"Context Capsule input must already use NFC text and LF line endings"
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
let capsule = parseContextCapsuleV1(input);
|
|
45
|
+
if (capsule.budget.estimator === "active_tokenizer") {
|
|
46
|
+
if (
|
|
47
|
+
authority.countTokens === undefined ||
|
|
48
|
+
authority.tokenizerFingerprint !== capsule.budget.tokenizerFingerprint
|
|
49
|
+
) {
|
|
50
|
+
throw new ContextCapsuleContractError(
|
|
51
|
+
"tokenizer_unavailable",
|
|
52
|
+
"The Capsule's active tokenizer and matching fingerprint are required for verification"
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
capsule = parseContextCapsuleV1(input, {
|
|
56
|
+
countTokens: authority.countTokens,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (rawInputBefore !== canonicalContextCapsuleJson(capsule)) {
|
|
60
|
+
throw new ContextCapsuleContractError(
|
|
61
|
+
"invalid_input",
|
|
62
|
+
"Context Capsule input must already use its canonical semantic representation"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return capsule;
|
|
66
|
+
};
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/** Deterministic, non-mutating verification for saved Context Capsules. */
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ChunkRow,
|
|
5
|
+
DocumentRow,
|
|
6
|
+
StorePort,
|
|
7
|
+
StoreResult,
|
|
8
|
+
} from "../store/types";
|
|
9
|
+
import type { ContextCapsuleV1 } from "./context-capsule";
|
|
10
|
+
import type { ContextCapsuleVerification } from "./context-capsule-verification";
|
|
11
|
+
|
|
12
|
+
import { decorateUriForIndex, deriveDocid } from "../app/constants";
|
|
13
|
+
import { chunkMatchesCanonicalContent } from "../pipeline/chunk-lookup";
|
|
14
|
+
import { type ContextCapsuleCreateOptions } from "./context-capsule";
|
|
15
|
+
import { sha256Text } from "./context-capsule-validation";
|
|
16
|
+
import {
|
|
17
|
+
CONTEXT_CAPSULE_FINGERPRINT_DRIFT_REASONS,
|
|
18
|
+
contextCapsuleVerificationSchema,
|
|
19
|
+
} from "./context-capsule-verification";
|
|
20
|
+
import {
|
|
21
|
+
captureContextEvidenceSnapshot,
|
|
22
|
+
type ContextEvidenceSnapshot,
|
|
23
|
+
} from "./context-evidence";
|
|
24
|
+
import { canonicalVerifierJson } from "./context-verifier-canonical";
|
|
25
|
+
import {
|
|
26
|
+
parseCanonicalContextCapsuleForVerification,
|
|
27
|
+
rawCanonicalContextJson,
|
|
28
|
+
} from "./context-verifier-input";
|
|
29
|
+
import { extractInclusiveLines } from "./sections";
|
|
30
|
+
|
|
31
|
+
type ContextVerifierStore = Pick<
|
|
32
|
+
StorePort,
|
|
33
|
+
| "getActivationIndexSnapshot"
|
|
34
|
+
| "getChunksBatch"
|
|
35
|
+
| "getCollections"
|
|
36
|
+
| "getContexts"
|
|
37
|
+
| "getDocumentsByDocids"
|
|
38
|
+
> &
|
|
39
|
+
Required<Pick<StorePort, "getContentBatch">>;
|
|
40
|
+
|
|
41
|
+
export interface ContextVerifierFingerprints {
|
|
42
|
+
config: string;
|
|
43
|
+
retrieval: string;
|
|
44
|
+
embeddingModel: string | null;
|
|
45
|
+
rerankModel: string | null;
|
|
46
|
+
tokenizer: string | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ContextVerifierDeps {
|
|
50
|
+
store: ContextVerifierStore;
|
|
51
|
+
currentFingerprints: ContextVerifierFingerprints;
|
|
52
|
+
resolveCurrentRanks?: (
|
|
53
|
+
capsule: ContextCapsuleV1
|
|
54
|
+
) => Promise<ReadonlyMap<string, number>>;
|
|
55
|
+
countTokens?: ContextCapsuleCreateOptions["countTokens"];
|
|
56
|
+
tokenizerFingerprint?: string | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type ContextVerifierErrorCode =
|
|
60
|
+
| "capsule_mutated_during_verify"
|
|
61
|
+
| "chunk_load_failed"
|
|
62
|
+
| "content_load_failed"
|
|
63
|
+
| "context_changed_during_verify"
|
|
64
|
+
| "document_load_failed"
|
|
65
|
+
| "index_changed_during_verify";
|
|
66
|
+
|
|
67
|
+
export class ContextVerifierError extends Error {
|
|
68
|
+
readonly code: ContextVerifierErrorCode;
|
|
69
|
+
|
|
70
|
+
constructor(
|
|
71
|
+
code: ContextVerifierErrorCode,
|
|
72
|
+
message: string,
|
|
73
|
+
cause?: unknown
|
|
74
|
+
) {
|
|
75
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
76
|
+
this.name = "ContextVerifierError";
|
|
77
|
+
this.code = code;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const unwrapStore = <T>(
|
|
82
|
+
result: StoreResult<T>,
|
|
83
|
+
code: ContextVerifierErrorCode,
|
|
84
|
+
operation: string
|
|
85
|
+
): T => {
|
|
86
|
+
if (result.ok) return result.value;
|
|
87
|
+
throw new ContextVerifierError(
|
|
88
|
+
code,
|
|
89
|
+
`${operation}: ${result.error.message}`,
|
|
90
|
+
result.error.cause
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const matchingDocument = (
|
|
95
|
+
documents: readonly DocumentRow[],
|
|
96
|
+
uri: string,
|
|
97
|
+
sourceHash: string,
|
|
98
|
+
mirrorHash: string,
|
|
99
|
+
indexName: string
|
|
100
|
+
): DocumentRow | null => {
|
|
101
|
+
const matches = documents.filter(
|
|
102
|
+
(document) =>
|
|
103
|
+
document.active &&
|
|
104
|
+
document.docid === deriveDocid(sourceHash) &&
|
|
105
|
+
document.sourceHash === sourceHash &&
|
|
106
|
+
document.mirrorHash === mirrorHash &&
|
|
107
|
+
decorateUriForIndex(document.uri, indexName) === uri
|
|
108
|
+
);
|
|
109
|
+
return matches.length === 1 ? (matches[0] ?? null) : null;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const rawInclusiveLines = (
|
|
113
|
+
content: string,
|
|
114
|
+
startLine: number,
|
|
115
|
+
endLine: number
|
|
116
|
+
): string => {
|
|
117
|
+
const lines = content.split("\n");
|
|
118
|
+
if (startLine > lines.length) return "";
|
|
119
|
+
return lines.slice(startLine - 1, Math.min(endLine, lines.length)).join("\n");
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
type Evidence = ContextCapsuleV1["evidence"][number];
|
|
123
|
+
type EvidenceReceipt = ContextCapsuleVerification["evidence"][number];
|
|
124
|
+
|
|
125
|
+
interface LoadedEvidence {
|
|
126
|
+
contentByHash: Map<string, string>;
|
|
127
|
+
chunksByHash: Map<string, ChunkRow[]>;
|
|
128
|
+
documents: DocumentRow[];
|
|
129
|
+
snapshot: ContextEvidenceSnapshot;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const missingReceipt = (evidence: Evidence): EvidenceReceipt => ({
|
|
133
|
+
evidenceId: evidence.evidenceId,
|
|
134
|
+
uri: evidence.uri,
|
|
135
|
+
contentStatus: "missing",
|
|
136
|
+
contentCode: "source_missing",
|
|
137
|
+
rankingStatus: "unavailable",
|
|
138
|
+
rankingCode: "ranking_unavailable",
|
|
139
|
+
currentSourceHash: null,
|
|
140
|
+
currentMirrorHash: null,
|
|
141
|
+
currentPassageHash: null,
|
|
142
|
+
currentRetrievalRank: null,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const missingMirrorReceipt = (
|
|
146
|
+
evidence: Evidence,
|
|
147
|
+
sourceHash: string,
|
|
148
|
+
mirrorHash: string | null
|
|
149
|
+
): EvidenceReceipt => ({
|
|
150
|
+
evidenceId: evidence.evidenceId,
|
|
151
|
+
uri: evidence.uri,
|
|
152
|
+
contentStatus: "missing",
|
|
153
|
+
contentCode: "mirror_missing",
|
|
154
|
+
rankingStatus: "unavailable",
|
|
155
|
+
rankingCode: "ranking_unavailable",
|
|
156
|
+
currentSourceHash: sourceHash,
|
|
157
|
+
currentMirrorHash: mirrorHash,
|
|
158
|
+
currentPassageHash: null,
|
|
159
|
+
currentRetrievalRank: null,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const verifyContent = (
|
|
163
|
+
evidence: Evidence,
|
|
164
|
+
loaded: LoadedEvidence,
|
|
165
|
+
indexName: string
|
|
166
|
+
): EvidenceReceipt => {
|
|
167
|
+
const identities = loaded.snapshot.documents.filter(
|
|
168
|
+
(document) => document.uri === evidence.uri
|
|
169
|
+
);
|
|
170
|
+
const identity = identities.length === 1 ? identities[0] : undefined;
|
|
171
|
+
if (!identity) return missingReceipt(evidence);
|
|
172
|
+
if (!identity.mirrorHash) {
|
|
173
|
+
return missingMirrorReceipt(evidence, identity.sourceHash, null);
|
|
174
|
+
}
|
|
175
|
+
const document = matchingDocument(
|
|
176
|
+
loaded.documents,
|
|
177
|
+
evidence.uri,
|
|
178
|
+
identity.sourceHash,
|
|
179
|
+
identity.mirrorHash,
|
|
180
|
+
indexName
|
|
181
|
+
);
|
|
182
|
+
const currentSourceHash = document?.sourceHash ?? identity.sourceHash;
|
|
183
|
+
const registeredMirrorHash = document?.mirrorHash ?? identity.mirrorHash;
|
|
184
|
+
const content = loaded.contentByHash.get(registeredMirrorHash);
|
|
185
|
+
if (content === undefined) {
|
|
186
|
+
return missingMirrorReceipt(
|
|
187
|
+
evidence,
|
|
188
|
+
currentSourceHash,
|
|
189
|
+
registeredMirrorHash
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const currentMirrorHash = sha256Text(content);
|
|
194
|
+
const exactPassage = extractInclusiveLines(
|
|
195
|
+
content,
|
|
196
|
+
evidence.startLine,
|
|
197
|
+
evidence.endLine
|
|
198
|
+
);
|
|
199
|
+
const currentPassageHash = sha256Text(
|
|
200
|
+
exactPassage ??
|
|
201
|
+
rawInclusiveLines(content, evidence.startLine, evidence.endLine)
|
|
202
|
+
);
|
|
203
|
+
const chunks = loaded.chunksByHash.get(registeredMirrorHash);
|
|
204
|
+
const chunk = chunks?.find(
|
|
205
|
+
(candidate) =>
|
|
206
|
+
candidate.mirrorHash === registeredMirrorHash &&
|
|
207
|
+
candidate.startLine === evidence.startLine &&
|
|
208
|
+
candidate.endLine === evidence.endLine &&
|
|
209
|
+
chunkMatchesCanonicalContent(candidate, content)
|
|
210
|
+
);
|
|
211
|
+
const chunkValid = chunk !== undefined;
|
|
212
|
+
|
|
213
|
+
let contentCode: EvidenceReceipt["contentCode"] = "verified_unchanged";
|
|
214
|
+
if (currentSourceHash !== evidence.sourceHash) {
|
|
215
|
+
contentCode = "source_stale";
|
|
216
|
+
} else if (
|
|
217
|
+
currentMirrorHash !== registeredMirrorHash ||
|
|
218
|
+
content.includes("\r")
|
|
219
|
+
) {
|
|
220
|
+
contentCode = "mirror_corrupt";
|
|
221
|
+
} else if (registeredMirrorHash !== evidence.mirrorHash) {
|
|
222
|
+
contentCode = "mirror_stale";
|
|
223
|
+
} else if (
|
|
224
|
+
exactPassage === null ||
|
|
225
|
+
currentPassageHash !== evidence.passageHash
|
|
226
|
+
) {
|
|
227
|
+
contentCode = "passage_stale";
|
|
228
|
+
} else if (!chunks || chunks.length === 0) {
|
|
229
|
+
contentCode = "chunk_missing";
|
|
230
|
+
} else if (!chunkValid) {
|
|
231
|
+
contentCode = "chunk_corrupt";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
evidenceId: evidence.evidenceId,
|
|
236
|
+
uri: evidence.uri,
|
|
237
|
+
contentStatus: contentCode === "verified_unchanged" ? "unchanged" : "stale",
|
|
238
|
+
contentCode,
|
|
239
|
+
rankingStatus: "unavailable",
|
|
240
|
+
rankingCode: "ranking_unavailable",
|
|
241
|
+
currentSourceHash,
|
|
242
|
+
currentMirrorHash,
|
|
243
|
+
currentPassageHash,
|
|
244
|
+
currentRetrievalRank: null,
|
|
245
|
+
};
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const applyRanking = (
|
|
249
|
+
receipt: EvidenceReceipt,
|
|
250
|
+
evidence: Evidence,
|
|
251
|
+
currentRanks: ReadonlyMap<string, number> | null
|
|
252
|
+
): EvidenceReceipt => {
|
|
253
|
+
if (receipt.contentStatus !== "unchanged") return receipt;
|
|
254
|
+
const rank = currentRanks?.get(evidence.evidenceId);
|
|
255
|
+
if (!Number.isSafeInteger(rank) || (rank ?? 0) < 1) return receipt;
|
|
256
|
+
const reranked = rank !== evidence.retrievalRank;
|
|
257
|
+
return {
|
|
258
|
+
...receipt,
|
|
259
|
+
rankingStatus: reranked ? "reranked" : "unchanged",
|
|
260
|
+
rankingCode: reranked ? "ranking_changed" : "ranking_unchanged",
|
|
261
|
+
currentRetrievalRank: rank ?? null,
|
|
262
|
+
};
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const aggregateReceipt = (
|
|
266
|
+
capsule: ContextCapsuleV1,
|
|
267
|
+
indexFingerprint: string,
|
|
268
|
+
currentFingerprints: ContextVerifierFingerprints,
|
|
269
|
+
evidence: EvidenceReceipt[]
|
|
270
|
+
): ContextCapsuleVerification => {
|
|
271
|
+
const contentStatus = evidence.some(
|
|
272
|
+
(item) => item.contentStatus === "missing"
|
|
273
|
+
)
|
|
274
|
+
? "missing"
|
|
275
|
+
: evidence.some((item) => item.contentStatus === "stale")
|
|
276
|
+
? "stale"
|
|
277
|
+
: "unchanged";
|
|
278
|
+
const rankingStatus = evidence.some(
|
|
279
|
+
(item) => item.rankingStatus === "unavailable"
|
|
280
|
+
)
|
|
281
|
+
? "unavailable"
|
|
282
|
+
: evidence.some((item) => item.rankingStatus === "reranked")
|
|
283
|
+
? "reranked"
|
|
284
|
+
: "unchanged";
|
|
285
|
+
const reasons = fingerprintReasons(
|
|
286
|
+
capsule,
|
|
287
|
+
currentFingerprints,
|
|
288
|
+
indexFingerprint
|
|
289
|
+
);
|
|
290
|
+
return contextCapsuleVerificationSchema.parse({
|
|
291
|
+
schemaVersion: capsule.schemaVersion,
|
|
292
|
+
coordinateSpace: capsule.coordinateSpace,
|
|
293
|
+
capsuleId: capsule.capsuleId,
|
|
294
|
+
operationStatus: "completed",
|
|
295
|
+
contentStatus,
|
|
296
|
+
contentCode:
|
|
297
|
+
contentStatus === "missing"
|
|
298
|
+
? "content_missing"
|
|
299
|
+
: contentStatus === "stale"
|
|
300
|
+
? "content_stale"
|
|
301
|
+
: "verified_unchanged",
|
|
302
|
+
rankingStatus,
|
|
303
|
+
rankingCode:
|
|
304
|
+
rankingStatus === "unavailable"
|
|
305
|
+
? "ranking_unavailable"
|
|
306
|
+
: rankingStatus === "reranked"
|
|
307
|
+
? "ranking_changed"
|
|
308
|
+
: "ranking_unchanged",
|
|
309
|
+
currentFingerprints: {
|
|
310
|
+
...currentFingerprints,
|
|
311
|
+
index: indexFingerprint,
|
|
312
|
+
},
|
|
313
|
+
fingerprintStatus: reasons.length === 0 ? "unchanged" : "drifted",
|
|
314
|
+
fingerprintReasons: reasons,
|
|
315
|
+
indexSnapshot: {
|
|
316
|
+
before: indexFingerprint,
|
|
317
|
+
after: indexFingerprint,
|
|
318
|
+
stable: true,
|
|
319
|
+
},
|
|
320
|
+
evidence,
|
|
321
|
+
});
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const fingerprintReasons = (
|
|
325
|
+
capsule: ContextCapsuleV1,
|
|
326
|
+
current: ContextVerifierFingerprints,
|
|
327
|
+
indexFingerprint: string
|
|
328
|
+
): (typeof CONTEXT_CAPSULE_FINGERPRINT_DRIFT_REASONS)[number][] => {
|
|
329
|
+
const changed = {
|
|
330
|
+
config_changed: current.config !== capsule.fingerprints.config,
|
|
331
|
+
retrieval_changed: current.retrieval !== capsule.fingerprints.retrieval,
|
|
332
|
+
embedding_model_changed:
|
|
333
|
+
current.embeddingModel !== capsule.fingerprints.embeddingModel,
|
|
334
|
+
rerank_model_changed:
|
|
335
|
+
current.rerankModel !== capsule.fingerprints.rerankModel,
|
|
336
|
+
tokenizer_changed: current.tokenizer !== capsule.fingerprints.tokenizer,
|
|
337
|
+
index_changed: indexFingerprint !== capsule.retrieval.indexSnapshot.after,
|
|
338
|
+
} satisfies Record<
|
|
339
|
+
(typeof CONTEXT_CAPSULE_FINGERPRINT_DRIFT_REASONS)[number],
|
|
340
|
+
boolean
|
|
341
|
+
>;
|
|
342
|
+
return CONTEXT_CAPSULE_FINGERPRINT_DRIFT_REASONS.filter(
|
|
343
|
+
(reason) => changed[reason]
|
|
344
|
+
);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
export { parseCanonicalContextCapsuleForVerification } from "./context-verifier-input";
|
|
348
|
+
|
|
349
|
+
/** Verify a Capsule without rebuilding it or mutating caller-owned input. */
|
|
350
|
+
export const verifyContextCapsule = async (
|
|
351
|
+
input: unknown,
|
|
352
|
+
deps: ContextVerifierDeps
|
|
353
|
+
): Promise<ContextCapsuleVerification> => {
|
|
354
|
+
const rawInputBefore = rawCanonicalContextJson(input);
|
|
355
|
+
const capsule = parseCanonicalContextCapsuleForVerification(input, deps);
|
|
356
|
+
const before = await captureContextEvidenceSnapshot(
|
|
357
|
+
deps.store,
|
|
358
|
+
capsule.scope.indexName,
|
|
359
|
+
capsule.scope.collections
|
|
360
|
+
);
|
|
361
|
+
const evidenceUris = new Set(capsule.evidence.map((item) => item.uri));
|
|
362
|
+
const referencedDocuments = before.documents.filter((document) =>
|
|
363
|
+
evidenceUris.has(document.uri)
|
|
364
|
+
);
|
|
365
|
+
const docids = [
|
|
366
|
+
...new Set(
|
|
367
|
+
referencedDocuments.map((document) => deriveDocid(document.sourceHash))
|
|
368
|
+
),
|
|
369
|
+
];
|
|
370
|
+
const mirrorHashes = [
|
|
371
|
+
...new Set(
|
|
372
|
+
referencedDocuments.flatMap((document) =>
|
|
373
|
+
document.mirrorHash === null ? [] : [document.mirrorHash]
|
|
374
|
+
)
|
|
375
|
+
),
|
|
376
|
+
];
|
|
377
|
+
const [documentResult, contentResult, chunkResult, rankResult] =
|
|
378
|
+
await Promise.all([
|
|
379
|
+
deps.store.getDocumentsByDocids(docids, { activeOnly: true }),
|
|
380
|
+
deps.store.getContentBatch(mirrorHashes),
|
|
381
|
+
deps.store.getChunksBatch(mirrorHashes),
|
|
382
|
+
deps.resolveCurrentRanks?.(structuredClone(capsule)).catch(() => null) ??
|
|
383
|
+
Promise.resolve(null),
|
|
384
|
+
]);
|
|
385
|
+
const loaded: LoadedEvidence = {
|
|
386
|
+
snapshot: before,
|
|
387
|
+
documents: unwrapStore(
|
|
388
|
+
documentResult,
|
|
389
|
+
"document_load_failed",
|
|
390
|
+
"Failed to batch-load verification documents"
|
|
391
|
+
),
|
|
392
|
+
contentByHash: unwrapStore(
|
|
393
|
+
contentResult,
|
|
394
|
+
"content_load_failed",
|
|
395
|
+
"Failed to batch-load verification mirrors"
|
|
396
|
+
),
|
|
397
|
+
chunksByHash: unwrapStore(
|
|
398
|
+
chunkResult,
|
|
399
|
+
"chunk_load_failed",
|
|
400
|
+
"Failed to batch-load verification chunks"
|
|
401
|
+
),
|
|
402
|
+
};
|
|
403
|
+
const after = await captureContextEvidenceSnapshot(
|
|
404
|
+
deps.store,
|
|
405
|
+
capsule.scope.indexName,
|
|
406
|
+
capsule.scope.collections
|
|
407
|
+
);
|
|
408
|
+
if (before.indexFingerprint !== after.indexFingerprint) {
|
|
409
|
+
throw new ContextVerifierError(
|
|
410
|
+
"index_changed_during_verify",
|
|
411
|
+
"Index changed while Context Capsule verification was running"
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
if (before.contextFingerprint !== after.contextFingerprint) {
|
|
415
|
+
throw new ContextVerifierError(
|
|
416
|
+
"context_changed_during_verify",
|
|
417
|
+
"Configured contexts changed while Context Capsule verification was running"
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
const rawInputAfter = rawCanonicalContextJson(input);
|
|
421
|
+
if (rawInputBefore !== rawInputAfter) {
|
|
422
|
+
throw new ContextVerifierError(
|
|
423
|
+
"capsule_mutated_during_verify",
|
|
424
|
+
"Context Capsule input changed while verification was running"
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const evidence = capsule.evidence.map((item) =>
|
|
429
|
+
applyRanking(
|
|
430
|
+
verifyContent(item, loaded, capsule.scope.indexName),
|
|
431
|
+
item,
|
|
432
|
+
rankResult
|
|
433
|
+
)
|
|
434
|
+
);
|
|
435
|
+
return aggregateReceipt(
|
|
436
|
+
capsule,
|
|
437
|
+
before.indexFingerprint,
|
|
438
|
+
deps.currentFingerprints,
|
|
439
|
+
evidence
|
|
440
|
+
);
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
/** Canonical JSON projection for cross-surface receipt parity. */
|
|
444
|
+
export const canonicalContextCapsuleVerificationJson = (
|
|
445
|
+
input: unknown
|
|
446
|
+
): string =>
|
|
447
|
+
canonicalVerifierJson(contextCapsuleVerificationSchema.parse(input));
|