@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
package/src/cli/program.ts
CHANGED
|
@@ -210,6 +210,42 @@ function parseCsvValues(raw: unknown): string[] | undefined {
|
|
|
210
210
|
return values.length > 0 ? values : undefined;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
function parseContextInteger(
|
|
214
|
+
name: string,
|
|
215
|
+
value: unknown,
|
|
216
|
+
allowZero = false
|
|
217
|
+
): number {
|
|
218
|
+
const raw = typeof value === "string" ? value : String(value);
|
|
219
|
+
if (!/^\d+$/.test(raw)) {
|
|
220
|
+
throw new CliError("VALIDATION", `--${name} must be an integer`, {
|
|
221
|
+
details: { contextCode: "invalid_budget" },
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const parsed = Number(raw);
|
|
225
|
+
if (!Number.isSafeInteger(parsed) || (allowZero ? parsed < 0 : parsed < 1)) {
|
|
226
|
+
throw new CliError(
|
|
227
|
+
"VALIDATION",
|
|
228
|
+
`--${name} must be ${allowZero ? "non-negative" : "positive"}`,
|
|
229
|
+
{ details: { contextCode: "invalid_budget" } }
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return parsed;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function validateContextOutputPath(value: unknown): string | undefined {
|
|
236
|
+
if (value === undefined) return undefined;
|
|
237
|
+
if (typeof value !== "string" || !value.trim() || value === "-") {
|
|
238
|
+
throw new CliError(
|
|
239
|
+
"VALIDATION",
|
|
240
|
+
"--output requires an explicit file path",
|
|
241
|
+
{
|
|
242
|
+
details: { contextCode: "invalid_filter" },
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
|
|
213
249
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
214
250
|
// Program Factory
|
|
215
251
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1624,6 +1660,148 @@ function wireManagementCommands(program: Command): void {
|
|
|
1624
1660
|
await contextCheck(format as "terminal" | "json" | "md");
|
|
1625
1661
|
});
|
|
1626
1662
|
|
|
1663
|
+
contextCmd
|
|
1664
|
+
.command("build <goal...>")
|
|
1665
|
+
.description("Build a deterministic evidence Capsule")
|
|
1666
|
+
.requiredOption("--budget <tokens>", "global token budget")
|
|
1667
|
+
.option("--bytes <bytes>", "global byte budget")
|
|
1668
|
+
.option("--query <query>", "retrieval query (defaults to goal)")
|
|
1669
|
+
.option(
|
|
1670
|
+
"--query-mode <mode:text>",
|
|
1671
|
+
"structured mode entry (repeatable): term:<text>, intent:<text>, or hyde:<text>",
|
|
1672
|
+
(value: string, previous: string[] = []) => [...previous, value],
|
|
1673
|
+
[]
|
|
1674
|
+
)
|
|
1675
|
+
.option(
|
|
1676
|
+
"-c, --collection <name>",
|
|
1677
|
+
"collection filter (repeatable)",
|
|
1678
|
+
(value: string, previous: string[] = []) => [...previous, value],
|
|
1679
|
+
[]
|
|
1680
|
+
)
|
|
1681
|
+
.option("--uri-prefix <uri>", "canonical GNO URI prefix")
|
|
1682
|
+
.option("--tags-all <tags>", "require ALL tags (comma-separated)")
|
|
1683
|
+
.option("--tags-any <tags>", "require ANY tag (comma-separated)")
|
|
1684
|
+
.option("--category <values>", "require category match (comma-separated)")
|
|
1685
|
+
.option("--author <text>", "filter by author")
|
|
1686
|
+
.option("--lang <code>", "language hint (BCP-47)")
|
|
1687
|
+
.option("--since <date>", "modified-at lower bound")
|
|
1688
|
+
.option("--until <date>", "modified-at upper bound")
|
|
1689
|
+
.option("--graph", "enable graph neighbor expansion")
|
|
1690
|
+
.option("--fast", "use lexical-first fast retrieval")
|
|
1691
|
+
.option("--thorough", "use a wider retrieval pool")
|
|
1692
|
+
.option("-n, --limit <num>", "maximum retrieved results")
|
|
1693
|
+
.option("-C, --candidate-limit <num>", "maximum rerank candidates")
|
|
1694
|
+
.option("--safety-margin <tokens>", "reserved token margin", "0")
|
|
1695
|
+
.option("--safety-margin-bytes <bytes>", "reserved byte margin", "0")
|
|
1696
|
+
.option("--output <path>", "write only to this explicit file")
|
|
1697
|
+
.option("--json", "canonical JSON output")
|
|
1698
|
+
.option("--md", "readable Markdown output")
|
|
1699
|
+
.action(async (goalParts: string[], cmdOpts: Record<string, unknown>) => {
|
|
1700
|
+
const format = getFormat(cmdOpts);
|
|
1701
|
+
assertFormatSupported(CMD.contextBuild, format);
|
|
1702
|
+
const outputPath = validateContextOutputPath(cmdOpts.output);
|
|
1703
|
+
if (cmdOpts.fast && cmdOpts.thorough) {
|
|
1704
|
+
throw new CliError("VALIDATION", "Choose either --fast or --thorough");
|
|
1705
|
+
}
|
|
1706
|
+
const globals = getGlobals();
|
|
1707
|
+
const { contextBuild } = await import("./commands/context-build");
|
|
1708
|
+
let queryModes: import("../pipeline/types").QueryModeInput[] | undefined;
|
|
1709
|
+
if (Array.isArray(cmdOpts.queryMode) && cmdOpts.queryMode.length > 0) {
|
|
1710
|
+
const { parseQueryModeSpecs } = await import("../pipeline/query-modes");
|
|
1711
|
+
const parsed = parseQueryModeSpecs(cmdOpts.queryMode as string[]);
|
|
1712
|
+
if (!parsed.ok) {
|
|
1713
|
+
throw new CliError("VALIDATION", parsed.error.message, {
|
|
1714
|
+
details: { contextCode: "invalid_filter" },
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
queryModes = parsed.value;
|
|
1718
|
+
}
|
|
1719
|
+
const output = await contextBuild(goalParts.join(" "), {
|
|
1720
|
+
configPath: globals.config,
|
|
1721
|
+
indexName: globals.index,
|
|
1722
|
+
budgetTokens: parseContextInteger("budget", cmdOpts.budget),
|
|
1723
|
+
budgetBytes:
|
|
1724
|
+
cmdOpts.bytes === undefined
|
|
1725
|
+
? undefined
|
|
1726
|
+
: parseContextInteger("bytes", cmdOpts.bytes),
|
|
1727
|
+
safetyMarginTokens: parseContextInteger(
|
|
1728
|
+
"safety-margin",
|
|
1729
|
+
cmdOpts.safetyMargin ?? "0",
|
|
1730
|
+
true
|
|
1731
|
+
),
|
|
1732
|
+
safetyMarginBytes: parseContextInteger(
|
|
1733
|
+
"safety-margin-bytes",
|
|
1734
|
+
cmdOpts.safetyMarginBytes ?? "0",
|
|
1735
|
+
true
|
|
1736
|
+
),
|
|
1737
|
+
query: cmdOpts.query as string | undefined,
|
|
1738
|
+
queryModes,
|
|
1739
|
+
collections: cmdOpts.collection as string[],
|
|
1740
|
+
uriPrefix: cmdOpts.uriPrefix as string | undefined,
|
|
1741
|
+
tagsAll: parseCsvValues(cmdOpts.tagsAll),
|
|
1742
|
+
tagsAny: parseCsvValues(cmdOpts.tagsAny),
|
|
1743
|
+
categories: parseCsvValues(cmdOpts.category),
|
|
1744
|
+
author: cmdOpts.author as string | undefined,
|
|
1745
|
+
lang: cmdOpts.lang as string | undefined,
|
|
1746
|
+
since: cmdOpts.since as string | undefined,
|
|
1747
|
+
until: cmdOpts.until as string | undefined,
|
|
1748
|
+
graph: Boolean(cmdOpts.graph),
|
|
1749
|
+
limit:
|
|
1750
|
+
cmdOpts.limit === undefined
|
|
1751
|
+
? undefined
|
|
1752
|
+
: parseContextInteger("limit", cmdOpts.limit),
|
|
1753
|
+
candidateLimit:
|
|
1754
|
+
cmdOpts.candidateLimit === undefined
|
|
1755
|
+
? undefined
|
|
1756
|
+
: parseContextInteger("candidate-limit", cmdOpts.candidateLimit),
|
|
1757
|
+
depthPolicy: cmdOpts.fast
|
|
1758
|
+
? "fast"
|
|
1759
|
+
: cmdOpts.thorough
|
|
1760
|
+
? "thorough"
|
|
1761
|
+
: "balanced",
|
|
1762
|
+
format: format === "json" ? "json" : "md",
|
|
1763
|
+
});
|
|
1764
|
+
if (outputPath !== undefined) {
|
|
1765
|
+
await Bun.write(outputPath, output);
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
await writeOutput(output, format === "json" ? "json" : "md");
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
contextCmd
|
|
1772
|
+
.command("verify <file>")
|
|
1773
|
+
.description("Verify a saved Context Capsule without rebuilding it")
|
|
1774
|
+
.option("--output <path>", "write only to this explicit file")
|
|
1775
|
+
.option("--json", "canonical JSON receipt")
|
|
1776
|
+
.option("--md", "readable Markdown receipt")
|
|
1777
|
+
.action(
|
|
1778
|
+
async (
|
|
1779
|
+
file: string,
|
|
1780
|
+
cmdOpts: Record<string, unknown>,
|
|
1781
|
+
command: Command
|
|
1782
|
+
) => {
|
|
1783
|
+
const format = getFormat(cmdOpts);
|
|
1784
|
+
assertFormatSupported(CMD.contextVerify, format);
|
|
1785
|
+
const outputPath = validateContextOutputPath(cmdOpts.output);
|
|
1786
|
+
const globals = getGlobals();
|
|
1787
|
+
const explicitIndexName =
|
|
1788
|
+
command.getOptionValueSourceWithGlobals("index") === "cli"
|
|
1789
|
+
? globals.index
|
|
1790
|
+
: undefined;
|
|
1791
|
+
const { contextVerify } = await import("./commands/context-verify");
|
|
1792
|
+
const output = await contextVerify(file, {
|
|
1793
|
+
configPath: globals.config,
|
|
1794
|
+
indexName: explicitIndexName,
|
|
1795
|
+
format: format === "json" ? "json" : "md",
|
|
1796
|
+
});
|
|
1797
|
+
if (outputPath !== undefined) {
|
|
1798
|
+
await Bun.write(outputPath, output);
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
await writeOutput(output, format === "json" ? "json" : "md");
|
|
1802
|
+
}
|
|
1803
|
+
);
|
|
1804
|
+
|
|
1627
1805
|
contextCmd
|
|
1628
1806
|
.command("rm <uri>")
|
|
1629
1807
|
.description("Remove context item")
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic evidence selection under one canonical payload budget.
|
|
3
|
+
*
|
|
4
|
+
* Candidate text is already materialized into its final extractive form. The
|
|
5
|
+
* caller-owned projector is the budget authority: it must build the complete
|
|
6
|
+
* canonical Capsule payload, including coverage, omissions, and guidance.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const CONTEXT_OMISSION_REASONS = [
|
|
10
|
+
"duplicate",
|
|
11
|
+
"overlap",
|
|
12
|
+
"global_budget",
|
|
13
|
+
"redundant_coverage",
|
|
14
|
+
"document_share_cap",
|
|
15
|
+
"filtered_by_scope",
|
|
16
|
+
"invalid_coordinates",
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
export type ContextOmissionReason = (typeof CONTEXT_OMISSION_REASONS)[number];
|
|
20
|
+
|
|
21
|
+
export type ContextGapReason =
|
|
22
|
+
| "facet_not_found"
|
|
23
|
+
| "global_budget_exhausted"
|
|
24
|
+
| "filtered_by_scope";
|
|
25
|
+
|
|
26
|
+
export interface ContextCandidateReference {
|
|
27
|
+
candidateId: string;
|
|
28
|
+
uri: string;
|
|
29
|
+
docid: string;
|
|
30
|
+
startLine: number | null;
|
|
31
|
+
endLine: number | null;
|
|
32
|
+
passageHash: string | null;
|
|
33
|
+
sourceHash: string;
|
|
34
|
+
mirrorHash: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MaterializedContextCandidate<
|
|
38
|
+
T = unknown,
|
|
39
|
+
> extends ContextCandidateReference {
|
|
40
|
+
startLine: number;
|
|
41
|
+
endLine: number;
|
|
42
|
+
passageHash: string;
|
|
43
|
+
text: string;
|
|
44
|
+
facets: string[];
|
|
45
|
+
retrievalRank: number;
|
|
46
|
+
value: T;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ContextOmission extends ContextCandidateReference {
|
|
50
|
+
reason: ContextOmissionReason;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ContextReasonCounts {
|
|
54
|
+
duplicate: number;
|
|
55
|
+
overlap: number;
|
|
56
|
+
global_budget: number;
|
|
57
|
+
redundant_coverage: number;
|
|
58
|
+
document_share_cap: number;
|
|
59
|
+
filtered_by_scope: number;
|
|
60
|
+
invalid_coordinates: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ContextCoverageState {
|
|
64
|
+
coveredFacets: string[];
|
|
65
|
+
unresolvedFacets: string[];
|
|
66
|
+
gaps: Array<{ facet: string; code: ContextGapReason }>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ContextSelectionState<T = unknown> {
|
|
70
|
+
selected: MaterializedContextCandidate<T>[];
|
|
71
|
+
omissions: ContextOmission[];
|
|
72
|
+
reasonCounts: ContextReasonCounts;
|
|
73
|
+
coverage: ContextCoverageState;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ContextCanonicalProjection<T = unknown> {
|
|
77
|
+
value: T;
|
|
78
|
+
usedBytes: number;
|
|
79
|
+
usedTokens: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ContextBudgetLimits {
|
|
83
|
+
requestedBytes: number;
|
|
84
|
+
requestedTokens: number;
|
|
85
|
+
safetyMarginBytes: number;
|
|
86
|
+
safetyMarginTokens: number;
|
|
87
|
+
/** Defaults to 3/5 of the spendable byte budget. */
|
|
88
|
+
documentShareNumerator?: number;
|
|
89
|
+
documentShareDenominator?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ContextSelectionOptions<T, P> {
|
|
93
|
+
candidates: MaterializedContextCandidate<T>[];
|
|
94
|
+
requestedFacets: string[];
|
|
95
|
+
initialOmissions?: ContextOmission[];
|
|
96
|
+
filteredFacetMatches?: ReadonlySet<string>;
|
|
97
|
+
limits: ContextBudgetLimits;
|
|
98
|
+
projectCanonical: (
|
|
99
|
+
state: ContextSelectionState<T>
|
|
100
|
+
) => ContextCanonicalProjection<P> | null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ContextSelectionResult<T, P> extends ContextSelectionState<T> {
|
|
104
|
+
projection: ContextCanonicalProjection<P> | null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const compareCodeUnits = (left: string, right: string): number => {
|
|
108
|
+
if (left < right) return -1;
|
|
109
|
+
if (left > right) return 1;
|
|
110
|
+
return 0;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const compareReferences = (
|
|
114
|
+
left: ContextCandidateReference,
|
|
115
|
+
right: ContextCandidateReference
|
|
116
|
+
): number =>
|
|
117
|
+
compareCodeUnits(left.uri, right.uri) ||
|
|
118
|
+
(left.startLine ?? 0) - (right.startLine ?? 0) ||
|
|
119
|
+
(left.endLine ?? 0) - (right.endLine ?? 0) ||
|
|
120
|
+
compareCodeUnits(left.sourceHash, right.sourceHash) ||
|
|
121
|
+
compareCodeUnits(left.candidateId, right.candidateId);
|
|
122
|
+
|
|
123
|
+
const compareOmissions = (
|
|
124
|
+
left: ContextOmission,
|
|
125
|
+
right: ContextOmission
|
|
126
|
+
): number =>
|
|
127
|
+
compareCodeUnits(left.reason, right.reason) || compareReferences(left, right);
|
|
128
|
+
|
|
129
|
+
const emptyReasonCounts = (): ContextReasonCounts => ({
|
|
130
|
+
duplicate: 0,
|
|
131
|
+
overlap: 0,
|
|
132
|
+
global_budget: 0,
|
|
133
|
+
redundant_coverage: 0,
|
|
134
|
+
document_share_cap: 0,
|
|
135
|
+
filtered_by_scope: 0,
|
|
136
|
+
invalid_coordinates: 0,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const dedupeOmissions = (omissions: ContextOmission[]): ContextOmission[] => {
|
|
140
|
+
const byId = new Map<string, ContextOmission>();
|
|
141
|
+
for (const omission of [...omissions].sort(compareOmissions)) {
|
|
142
|
+
if (!byId.has(omission.candidateId)) {
|
|
143
|
+
byId.set(omission.candidateId, omission);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return [...byId.values()].sort(compareOmissions);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const countReasons = (omissions: ContextOmission[]): ContextReasonCounts => {
|
|
150
|
+
const counts = emptyReasonCounts();
|
|
151
|
+
for (const omission of omissions) counts[omission.reason] += 1;
|
|
152
|
+
return counts;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const coveredFacetSet = <T>(
|
|
156
|
+
selected: MaterializedContextCandidate<T>[]
|
|
157
|
+
): Set<string> => new Set(selected.flatMap((candidate) => candidate.facets));
|
|
158
|
+
|
|
159
|
+
const buildCoverage = <T>(
|
|
160
|
+
requestedFacets: string[],
|
|
161
|
+
selected: MaterializedContextCandidate<T>[],
|
|
162
|
+
candidates: MaterializedContextCandidate<T>[],
|
|
163
|
+
filteredFacetMatches: ReadonlySet<string>
|
|
164
|
+
): ContextCoverageState => {
|
|
165
|
+
const covered = coveredFacetSet(selected);
|
|
166
|
+
const available = new Set(
|
|
167
|
+
candidates.flatMap((candidate) => candidate.facets)
|
|
168
|
+
);
|
|
169
|
+
const coveredFacets = requestedFacets.filter((facet) => covered.has(facet));
|
|
170
|
+
const unresolvedFacets = requestedFacets.filter(
|
|
171
|
+
(facet) => !covered.has(facet)
|
|
172
|
+
);
|
|
173
|
+
const gaps = unresolvedFacets.map((facet) => ({
|
|
174
|
+
facet,
|
|
175
|
+
code: available.has(facet)
|
|
176
|
+
? ("global_budget_exhausted" as const)
|
|
177
|
+
: filteredFacetMatches.has(facet)
|
|
178
|
+
? ("filtered_by_scope" as const)
|
|
179
|
+
: ("facet_not_found" as const),
|
|
180
|
+
}));
|
|
181
|
+
return { coveredFacets, unresolvedFacets, gaps };
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const buildState = <T>(
|
|
185
|
+
requestedFacets: string[],
|
|
186
|
+
selected: MaterializedContextCandidate<T>[],
|
|
187
|
+
omissions: ContextOmission[],
|
|
188
|
+
candidates: MaterializedContextCandidate<T>[],
|
|
189
|
+
filteredFacetMatches: ReadonlySet<string>
|
|
190
|
+
): ContextSelectionState<T> => {
|
|
191
|
+
const orderedOmissions = dedupeOmissions(omissions);
|
|
192
|
+
return {
|
|
193
|
+
selected: selected.map((candidate) => ({ ...candidate })),
|
|
194
|
+
omissions: orderedOmissions,
|
|
195
|
+
reasonCounts: countReasons(orderedOmissions),
|
|
196
|
+
coverage: buildCoverage(
|
|
197
|
+
requestedFacets,
|
|
198
|
+
selected,
|
|
199
|
+
candidates,
|
|
200
|
+
filteredFacetMatches
|
|
201
|
+
),
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const overlaps = <T>(
|
|
206
|
+
candidate: MaterializedContextCandidate<T>,
|
|
207
|
+
selected: MaterializedContextCandidate<T>[]
|
|
208
|
+
): boolean =>
|
|
209
|
+
selected.some(
|
|
210
|
+
(item) =>
|
|
211
|
+
item.docid === candidate.docid &&
|
|
212
|
+
candidate.startLine <= item.endLine &&
|
|
213
|
+
item.startLine <= candidate.endLine
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const utf8Bytes = (value: string): number =>
|
|
217
|
+
new TextEncoder().encode(value).byteLength;
|
|
218
|
+
|
|
219
|
+
const validateLimits = (limits: ContextBudgetLimits): void => {
|
|
220
|
+
const values = [
|
|
221
|
+
limits.requestedBytes,
|
|
222
|
+
limits.requestedTokens,
|
|
223
|
+
limits.safetyMarginBytes,
|
|
224
|
+
limits.safetyMarginTokens,
|
|
225
|
+
];
|
|
226
|
+
if (
|
|
227
|
+
values.some((value) => !Number.isSafeInteger(value) || value < 0) ||
|
|
228
|
+
limits.requestedBytes <= limits.safetyMarginBytes ||
|
|
229
|
+
limits.requestedTokens <= limits.safetyMarginTokens
|
|
230
|
+
) {
|
|
231
|
+
throw new Error("Context budget limits must be positive safe integers");
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const projectionFits = <P>(
|
|
236
|
+
projection: ContextCanonicalProjection<P> | null,
|
|
237
|
+
limits: ContextBudgetLimits
|
|
238
|
+
): projection is ContextCanonicalProjection<P> =>
|
|
239
|
+
projection !== null &&
|
|
240
|
+
Number.isSafeInteger(projection.usedBytes) &&
|
|
241
|
+
Number.isSafeInteger(projection.usedTokens) &&
|
|
242
|
+
projection.usedBytes >= 0 &&
|
|
243
|
+
projection.usedTokens >= 0 &&
|
|
244
|
+
projection.usedBytes + limits.safetyMarginBytes <= limits.requestedBytes &&
|
|
245
|
+
projection.usedTokens + limits.safetyMarginTokens <= limits.requestedTokens;
|
|
246
|
+
|
|
247
|
+
const documentShareExceeded = <T>(
|
|
248
|
+
candidate: MaterializedContextCandidate<T>,
|
|
249
|
+
selected: MaterializedContextCandidate<T>[],
|
|
250
|
+
candidateDocumentCount: number,
|
|
251
|
+
limits: ContextBudgetLimits
|
|
252
|
+
): boolean => {
|
|
253
|
+
if (candidateDocumentCount <= 1) return false;
|
|
254
|
+
const numerator = limits.documentShareNumerator ?? 3;
|
|
255
|
+
const denominator = limits.documentShareDenominator ?? 5;
|
|
256
|
+
if (
|
|
257
|
+
!Number.isSafeInteger(numerator) ||
|
|
258
|
+
!Number.isSafeInteger(denominator) ||
|
|
259
|
+
numerator < 1 ||
|
|
260
|
+
denominator < numerator
|
|
261
|
+
) {
|
|
262
|
+
throw new Error("Invalid Context document share ratio");
|
|
263
|
+
}
|
|
264
|
+
const spendableBytes = limits.requestedBytes - limits.safetyMarginBytes;
|
|
265
|
+
const shareLimit = Number(
|
|
266
|
+
(BigInt(spendableBytes) * BigInt(numerator)) / BigInt(denominator)
|
|
267
|
+
);
|
|
268
|
+
const sameDocumentBytes = selected
|
|
269
|
+
.filter((item) => item.docid === candidate.docid)
|
|
270
|
+
.reduce((sum, item) => sum + utf8Bytes(item.text), 0);
|
|
271
|
+
return sameDocumentBytes + utf8Bytes(candidate.text) > shareLimit;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const omissionReason = <T>(
|
|
275
|
+
candidate: MaterializedContextCandidate<T>,
|
|
276
|
+
selected: MaterializedContextCandidate<T>[],
|
|
277
|
+
covered: ReadonlySet<string>,
|
|
278
|
+
requestedFacetCount: number,
|
|
279
|
+
candidateDocumentCount: number,
|
|
280
|
+
limits: ContextBudgetLimits
|
|
281
|
+
): ContextOmissionReason | null => {
|
|
282
|
+
if (requestedFacetCount > 0 && candidate.facets.length === 0) {
|
|
283
|
+
return "redundant_coverage";
|
|
284
|
+
}
|
|
285
|
+
if (overlaps(candidate, selected)) return "overlap";
|
|
286
|
+
if (
|
|
287
|
+
documentShareExceeded(candidate, selected, candidateDocumentCount, limits)
|
|
288
|
+
) {
|
|
289
|
+
return "document_share_cap";
|
|
290
|
+
}
|
|
291
|
+
if (
|
|
292
|
+
selected.length > 0 &&
|
|
293
|
+
candidate.facets.every((facet) => covered.has(facet))
|
|
294
|
+
) {
|
|
295
|
+
return "redundant_coverage";
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const asOmission = <T>(
|
|
301
|
+
candidate: MaterializedContextCandidate<T>,
|
|
302
|
+
reason: ContextOmissionReason
|
|
303
|
+
): ContextOmission => ({
|
|
304
|
+
candidateId: candidate.candidateId,
|
|
305
|
+
uri: candidate.uri,
|
|
306
|
+
docid: candidate.docid,
|
|
307
|
+
startLine: candidate.startLine,
|
|
308
|
+
endLine: candidate.endLine,
|
|
309
|
+
passageHash: candidate.passageHash,
|
|
310
|
+
sourceHash: candidate.sourceHash,
|
|
311
|
+
mirrorHash: candidate.mirrorHash,
|
|
312
|
+
reason,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const collapseDuplicates = <T>(
|
|
316
|
+
candidates: MaterializedContextCandidate<T>[]
|
|
317
|
+
): {
|
|
318
|
+
candidates: MaterializedContextCandidate<T>[];
|
|
319
|
+
omissions: ContextOmission[];
|
|
320
|
+
} => {
|
|
321
|
+
const ordered = [...candidates].sort(
|
|
322
|
+
(left, right) =>
|
|
323
|
+
left.retrievalRank - right.retrievalRank || compareReferences(left, right)
|
|
324
|
+
);
|
|
325
|
+
const seenIds = new Set<string>();
|
|
326
|
+
const seenPassages = new Set<string>();
|
|
327
|
+
const kept: MaterializedContextCandidate<T>[] = [];
|
|
328
|
+
const omissions: ContextOmission[] = [];
|
|
329
|
+
for (const candidate of ordered) {
|
|
330
|
+
if (seenIds.has(candidate.candidateId)) continue;
|
|
331
|
+
seenIds.add(candidate.candidateId);
|
|
332
|
+
if (seenPassages.has(candidate.passageHash)) {
|
|
333
|
+
omissions.push(asOmission(candidate, "duplicate"));
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
seenPassages.add(candidate.passageHash);
|
|
337
|
+
kept.push(candidate);
|
|
338
|
+
}
|
|
339
|
+
return { candidates: kept, omissions };
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const compareMarginalValue = <T>(
|
|
343
|
+
left: MaterializedContextCandidate<T>,
|
|
344
|
+
right: MaterializedContextCandidate<T>,
|
|
345
|
+
covered: ReadonlySet<string>,
|
|
346
|
+
poolSize: number
|
|
347
|
+
): number => {
|
|
348
|
+
const uncovered = (candidate: MaterializedContextCandidate<T>): number =>
|
|
349
|
+
candidate.facets.filter((facet) => !covered.has(facet)).length;
|
|
350
|
+
const leftUncovered = uncovered(left);
|
|
351
|
+
const rightUncovered = uncovered(right);
|
|
352
|
+
const leftCost = BigInt(Math.max(1, utf8Bytes(left.text)));
|
|
353
|
+
const rightCost = BigInt(Math.max(1, utf8Bytes(right.text)));
|
|
354
|
+
const leftRatio = BigInt(leftUncovered) * rightCost;
|
|
355
|
+
const rightRatio = BigInt(rightUncovered) * leftCost;
|
|
356
|
+
if (leftRatio !== rightRatio) return leftRatio > rightRatio ? -1 : 1;
|
|
357
|
+
const relevance = (candidate: MaterializedContextCandidate<T>): number =>
|
|
358
|
+
Math.max(1, poolSize - candidate.retrievalRank + 1);
|
|
359
|
+
const relevanceDifference = relevance(right) - relevance(left);
|
|
360
|
+
if (relevanceDifference !== 0) return relevanceDifference;
|
|
361
|
+
if (left.retrievalRank !== right.retrievalRank) {
|
|
362
|
+
return left.retrievalRank - right.retrievalRank;
|
|
363
|
+
}
|
|
364
|
+
return compareReferences(left, right);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
/** Select materialized candidates using exact full-payload fit checks. */
|
|
368
|
+
export const selectContextEvidence = <T, P>(
|
|
369
|
+
options: ContextSelectionOptions<T, P>
|
|
370
|
+
): ContextSelectionResult<T, P> => {
|
|
371
|
+
validateLimits(options.limits);
|
|
372
|
+
const requestedFacets = [...new Set(options.requestedFacets)].sort(
|
|
373
|
+
compareCodeUnits
|
|
374
|
+
);
|
|
375
|
+
const filteredFacetMatches =
|
|
376
|
+
options.filteredFacetMatches ?? new Set<string>();
|
|
377
|
+
const collapsed = collapseDuplicates(options.candidates);
|
|
378
|
+
const candidates = collapsed.candidates;
|
|
379
|
+
const candidateDocumentCount = new Set(
|
|
380
|
+
candidates.map((candidate) => candidate.docid)
|
|
381
|
+
).size;
|
|
382
|
+
const selected: MaterializedContextCandidate<T>[] = [];
|
|
383
|
+
const omissions = [
|
|
384
|
+
...(options.initialOmissions ?? []),
|
|
385
|
+
...collapsed.omissions,
|
|
386
|
+
];
|
|
387
|
+
const remaining = [...candidates];
|
|
388
|
+
let projection: ContextCanonicalProjection<P> | null = null;
|
|
389
|
+
|
|
390
|
+
while (remaining.length > 0) {
|
|
391
|
+
const covered = coveredFacetSet(selected);
|
|
392
|
+
const eligible: MaterializedContextCandidate<T>[] = [];
|
|
393
|
+
for (const candidate of remaining) {
|
|
394
|
+
const reason = omissionReason(
|
|
395
|
+
candidate,
|
|
396
|
+
selected,
|
|
397
|
+
covered,
|
|
398
|
+
requestedFacets.length,
|
|
399
|
+
candidateDocumentCount,
|
|
400
|
+
options.limits
|
|
401
|
+
);
|
|
402
|
+
if (reason) omissions.push(asOmission(candidate, reason));
|
|
403
|
+
else eligible.push(candidate);
|
|
404
|
+
}
|
|
405
|
+
remaining.length = 0;
|
|
406
|
+
remaining.push(...eligible);
|
|
407
|
+
if (remaining.length === 0) break;
|
|
408
|
+
|
|
409
|
+
remaining.sort((left, right) =>
|
|
410
|
+
compareMarginalValue(left, right, covered, candidates.length)
|
|
411
|
+
);
|
|
412
|
+
const candidate = remaining.shift();
|
|
413
|
+
if (!candidate) break;
|
|
414
|
+
const proposedSelected = [...selected, candidate];
|
|
415
|
+
const proposedCovered = coveredFacetSet(proposedSelected);
|
|
416
|
+
const provisionalOmissions = [
|
|
417
|
+
...omissions,
|
|
418
|
+
...remaining.map((item) =>
|
|
419
|
+
asOmission(
|
|
420
|
+
item,
|
|
421
|
+
omissionReason(
|
|
422
|
+
item,
|
|
423
|
+
proposedSelected,
|
|
424
|
+
proposedCovered,
|
|
425
|
+
requestedFacets.length,
|
|
426
|
+
candidateDocumentCount,
|
|
427
|
+
options.limits
|
|
428
|
+
) ?? "global_budget"
|
|
429
|
+
)
|
|
430
|
+
),
|
|
431
|
+
];
|
|
432
|
+
const state = buildState(
|
|
433
|
+
requestedFacets,
|
|
434
|
+
proposedSelected,
|
|
435
|
+
provisionalOmissions,
|
|
436
|
+
candidates,
|
|
437
|
+
filteredFacetMatches
|
|
438
|
+
);
|
|
439
|
+
const proposedProjection = options.projectCanonical(state);
|
|
440
|
+
if (projectionFits(proposedProjection, options.limits)) {
|
|
441
|
+
selected.push(candidate);
|
|
442
|
+
projection = proposedProjection;
|
|
443
|
+
} else {
|
|
444
|
+
omissions.push(asOmission(candidate, "global_budget"));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const finalState = buildState(
|
|
449
|
+
requestedFacets,
|
|
450
|
+
selected,
|
|
451
|
+
omissions,
|
|
452
|
+
candidates,
|
|
453
|
+
filteredFacetMatches
|
|
454
|
+
);
|
|
455
|
+
const finalProjection =
|
|
456
|
+
selected.length > 0 ? options.projectCanonical(finalState) : null;
|
|
457
|
+
if (selected.length > 0 && !projectionFits(finalProjection, options.limits)) {
|
|
458
|
+
throw new Error("Final canonical Context projection exceeds its budget");
|
|
459
|
+
}
|
|
460
|
+
return { ...finalState, projection: finalProjection ?? projection };
|
|
461
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
4
|
+
|
|
5
|
+
export const contextCapsuleIndexSnapshotSchema = z
|
|
6
|
+
.object({
|
|
7
|
+
before: sha256Schema,
|
|
8
|
+
after: sha256Schema,
|
|
9
|
+
stable: z.literal(true),
|
|
10
|
+
})
|
|
11
|
+
.strict()
|
|
12
|
+
.refine((value) => value.before === value.after, {
|
|
13
|
+
message: "index changed while the operation was running",
|
|
14
|
+
path: ["after"],
|
|
15
|
+
});
|