@gmickel/gno 2.7.1 → 2.8.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 +3 -2
- package/assets/skill/SKILL.md +8 -1
- package/assets/skill/cli-reference.md +8 -1
- package/assets/skill/examples.md +2 -1
- package/assets/skill/mcp-reference.md +3 -1
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.7.1.zip → gno-browser-clipper-v2.8.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.8.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +48 -6
- package/spec/db/schema.sql +0 -1
- package/spec/mcp.md +18 -3
- package/spec/output-schemas/audit-report.schema.json +18 -4
- package/spec/output-schemas/backlinks.schema.json +4 -0
- package/spec/output-schemas/collection-list.schema.json +13 -0
- package/spec/output-schemas/graph.schema.json +2 -0
- package/spec/output-schemas/links-list.schema.json +4 -0
- package/spec/output-schemas/status.schema.json +15 -0
- package/src/cli/commands/audit.ts +23 -4
- package/src/cli/commands/collection/list.ts +39 -5
- package/src/cli/commands/embed.ts +3 -3
- package/src/cli/commands/links.ts +34 -131
- package/src/cli/commands/shared.ts +7 -0
- package/src/cli/commands/status.ts +5 -0
- package/src/cli/program.ts +12 -2
- package/src/config/loader.ts +43 -0
- package/src/config/types.ts +8 -0
- package/src/core/audit-contract.ts +16 -4
- package/src/core/audit-freshness.ts +11 -1
- package/src/core/audit-links.ts +145 -25
- package/src/core/audit-provenance.ts +11 -4
- package/src/core/audit-workspace.ts +19 -4
- package/src/core/audit.ts +67 -15
- package/src/core/context-compiler.ts +3 -0
- package/src/core/context-evidence.ts +11 -0
- package/src/core/graph-edge-confidence.ts +23 -1
- package/src/core/host-paths.ts +1 -0
- package/src/core/knowledge-impact.ts +28 -0
- package/src/core/link-workspace.ts +324 -0
- package/src/core/retrieval-replay-candidate.ts +6 -0
- package/src/core/retrieval-trace-request.ts +3 -0
- package/src/index.ts +14 -1
- package/src/ingestion/graph-reconciliation.ts +77 -15
- package/src/ingestion/source-availability/darwin-path.ts +9 -3
- package/src/ingestion/sync.ts +22 -1
- package/src/ingestion/types.ts +14 -0
- package/src/llm/inference-scope.ts +4 -3
- package/src/mcp/http-egress.ts +42 -3
- package/src/mcp/tools/audit.ts +11 -2
- package/src/mcp/tools/changes.ts +1 -0
- package/src/mcp/tools/links.ts +3 -0
- package/src/mcp/tools/sessions.ts +33 -4
- package/src/mcp/tools/status.ts +3 -0
- package/src/pipeline/expansion.ts +19 -31
- package/src/pipeline/graph-retrieval.ts +22 -2
- package/src/pipeline/hybrid.ts +1 -1
- package/src/pipeline/types.ts +6 -3
- package/src/serve/findings-pass.ts +1 -1
- package/src/serve/public/pages/GraphView.tsx +2 -0
- package/src/serve/routes/changes.ts +6 -1
- package/src/serve/routes/links.ts +13 -0
- package/src/serve/routes/sessions.ts +41 -53
- package/src/sessions/config-refresh.ts +111 -0
- package/src/store/migrations/033-drop-documents-active-index.ts +30 -0
- package/src/store/migrations/034-collection-link-workspace.ts +47 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +390 -231
- package/src/store/sqlite/eligibility.ts +8 -2
- package/src/store/sqlite/graph-link-resolver.ts +252 -5
- package/src/store/sqlite/graph-neighbors.ts +147 -40
- package/src/store/sqlite/graph-reference-state.ts +13 -2
- package/src/store/sqlite/workspace-link-resolver.ts +654 -0
- package/src/store/types.ts +49 -3
- package/src/store/vector/stats.ts +1 -1
- package/browser-extension/artifacts/gno-browser-clipper-v2.7.1.zip.sha256 +0 -1
package/src/mcp/http-egress.ts
CHANGED
|
@@ -124,6 +124,21 @@ const collectionFromRef = (value: unknown): string | null => {
|
|
|
124
124
|
return value.slice(0, slash).trim().toLowerCase() || null;
|
|
125
125
|
};
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Tools whose results follow resolved graph edges. Links resolve across a
|
|
129
|
+
* link workspace, so a ref's own collection does not bound the result: the
|
|
130
|
+
* scope is the explicit collection argument (else every collection) plus the
|
|
131
|
+
* collection of every referenced document.
|
|
132
|
+
*/
|
|
133
|
+
const GRAPH_RESULT_TOOLS = new Set([
|
|
134
|
+
"gno_backlinks",
|
|
135
|
+
"gno_graph",
|
|
136
|
+
"gno_graph_neighbors",
|
|
137
|
+
"gno_graph_path",
|
|
138
|
+
"gno_graph_query",
|
|
139
|
+
"gno_impact",
|
|
140
|
+
]);
|
|
141
|
+
|
|
127
142
|
const requestedCollections = (
|
|
128
143
|
params: unknown,
|
|
129
144
|
collections: readonly Collection[]
|
|
@@ -134,17 +149,41 @@ const requestedCollections = (
|
|
|
134
149
|
|
|
135
150
|
const names = new Set<string>();
|
|
136
151
|
const direct = args.collection;
|
|
137
|
-
|
|
138
|
-
|
|
152
|
+
// Handlers treat a blank collection as omitted, so it must not count as a
|
|
153
|
+
// scope here either (a graph call with one is authorized as unscoped).
|
|
154
|
+
if (typeof direct === "string" && direct.trim())
|
|
155
|
+
names.add(direct.trim().toLowerCase());
|
|
156
|
+
if (
|
|
157
|
+
(record?.name === "gno_audit" || record?.name === "gno_impact") &&
|
|
158
|
+
Array.isArray(args.collections)
|
|
159
|
+
) {
|
|
139
160
|
for (const value of args.collections) {
|
|
140
161
|
if (typeof value !== "string") continue;
|
|
141
162
|
const normalized = value.trim().toLowerCase();
|
|
142
163
|
if (normalized) names.add(normalized);
|
|
143
164
|
}
|
|
144
165
|
}
|
|
166
|
+
const graphTool =
|
|
167
|
+
typeof record?.name === "string" && GRAPH_RESULT_TOOLS.has(record.name);
|
|
168
|
+
// A graph result spans every collection a link resolves into: without an
|
|
169
|
+
// explicit scope, authorize them all.
|
|
170
|
+
// `gno_similar` with crossCollection returns documents from every collection.
|
|
171
|
+
const crossCollectionSimilar =
|
|
172
|
+
record?.name === "gno_similar" && args.crossCollection === true;
|
|
173
|
+
if ((graphTool && names.size === 0) || crossCollectionSimilar) {
|
|
174
|
+
for (const { name } of collections) names.add(name);
|
|
175
|
+
}
|
|
176
|
+
// The referenced documents' own collections are always authorized too:
|
|
177
|
+
// graph tools serialize the target's metadata even when the result scope
|
|
178
|
+
// is narrower. A graph-tool ref whose collection cannot be read without
|
|
179
|
+
// the index (a docid) authorizes every collection (fail closed).
|
|
145
180
|
for (const key of ["ref", "target", "from", "to", "root", "uri"]) {
|
|
146
|
-
const
|
|
181
|
+
const value = args[key];
|
|
182
|
+
const collection = collectionFromRef(value);
|
|
147
183
|
if (collection) names.add(collection);
|
|
184
|
+
else if (graphTool && typeof value === "string" && value.trim()) {
|
|
185
|
+
for (const { name } of collections) names.add(name);
|
|
186
|
+
}
|
|
148
187
|
}
|
|
149
188
|
if (Array.isArray(args.refs)) {
|
|
150
189
|
for (const ref of args.refs) {
|
package/src/mcp/tools/audit.ts
CHANGED
|
@@ -5,7 +5,11 @@ import { z } from "zod";
|
|
|
5
5
|
import type { AuditCategory, AuditReport } from "../../core/audit";
|
|
6
6
|
import type { ToolContext } from "../server";
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
AUDIT_CATEGORIES,
|
|
10
|
+
AUDIT_MAX_FINDINGS_ALL,
|
|
11
|
+
AUDIT_MAX_FINDINGS_LIMIT,
|
|
12
|
+
} from "../../core/audit";
|
|
9
13
|
import { runWorkspaceAudit } from "../../core/audit-workspace";
|
|
10
14
|
import { normalizeTag, validateTag } from "../../core/tags";
|
|
11
15
|
import { normalizeCollectionName } from "../../core/validation";
|
|
@@ -19,7 +23,12 @@ export const auditInputSchema = z
|
|
|
19
23
|
collections: z.array(z.string().min(1)).max(256).default([]),
|
|
20
24
|
paths: z.array(z.string().min(1)).max(256).default([]),
|
|
21
25
|
tags: z.array(z.string().min(1)).max(256).default([]),
|
|
22
|
-
maxFindings: z
|
|
26
|
+
maxFindings: z
|
|
27
|
+
.union([
|
|
28
|
+
z.number().int().min(1).max(AUDIT_MAX_FINDINGS_LIMIT),
|
|
29
|
+
z.literal(AUDIT_MAX_FINDINGS_ALL),
|
|
30
|
+
])
|
|
31
|
+
.default(100),
|
|
23
32
|
maxAgeDays: z.number().int().min(1).optional(),
|
|
24
33
|
orphanRoots: z.array(z.string().min(1)).max(256).default([]),
|
|
25
34
|
orphanIgnorePrefixes: z.array(z.string().min(1)).max(256).default([]),
|
package/src/mcp/tools/changes.ts
CHANGED
|
@@ -24,6 +24,7 @@ export const diffInputSchema = z.object({
|
|
|
24
24
|
|
|
25
25
|
export const impactInputSchema = z.object({
|
|
26
26
|
ref: z.string().trim().min(1).max(4096),
|
|
27
|
+
collections: z.array(z.string().trim().min(1)).max(256).optional(),
|
|
27
28
|
maxDepth: z.number().int().min(1).max(6).default(3),
|
|
28
29
|
maxNodes: z.number().int().min(1).max(1000).default(100),
|
|
29
30
|
maxEdges: z.number().int().min(1).max(5000).default(250),
|
package/src/mcp/tools/links.ts
CHANGED
|
@@ -219,6 +219,8 @@ interface BacklinksInput {
|
|
|
219
219
|
interface BacklinkOutput {
|
|
220
220
|
sourceDocUri: string;
|
|
221
221
|
sourceDocTitle?: string;
|
|
222
|
+
/** Collection of the linking document. */
|
|
223
|
+
sourceCollection?: string;
|
|
222
224
|
linkText?: string;
|
|
223
225
|
position: { startLine: number; startCol: number };
|
|
224
226
|
}
|
|
@@ -308,6 +310,7 @@ export function handleBacklinks(
|
|
|
308
310
|
(b: BacklinkRow) => ({
|
|
309
311
|
sourceDocUri: b.sourceDocUri,
|
|
310
312
|
...(b.sourceDocTitle && { sourceDocTitle: b.sourceDocTitle }),
|
|
313
|
+
...(b.sourceCollection && { sourceCollection: b.sourceCollection }),
|
|
311
314
|
...(b.linkText && { linkText: b.linkText }),
|
|
312
315
|
position: { startLine: b.startLine, startCol: b.startCol },
|
|
313
316
|
})
|
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
|
|
14
14
|
import { z } from "zod";
|
|
15
15
|
|
|
16
|
+
import type { Config } from "../../config/types";
|
|
16
17
|
import type { ToolContext } from "../server";
|
|
17
18
|
|
|
18
19
|
import { runAutomationProfile } from "../../sessions/automation";
|
|
20
|
+
import { refreshServedConfig } from "../../sessions/config-refresh";
|
|
19
21
|
import {
|
|
20
22
|
formatAutomationRunText,
|
|
21
23
|
formatImportReceiptText,
|
|
@@ -99,9 +101,36 @@ export const SESSIONS_IMPORT_MCP_ANNOTATIONS = {
|
|
|
99
101
|
openWorldHint: false,
|
|
100
102
|
} as const;
|
|
101
103
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
/**
|
|
105
|
+
* The config file as it is now, adopted when the CLI changed it while this
|
|
106
|
+
* server runs (same binding checks and errors as the REST routes). Inside a
|
|
107
|
+
* request `ctx.config` stays the snapshot taken at its start, so callers use
|
|
108
|
+
* the returned config. An adoption moves the egress policy epoch; this
|
|
109
|
+
* request advances with it rather than voiding itself.
|
|
110
|
+
*/
|
|
111
|
+
function currentConfig(ctx: ToolContext): Promise<Config> {
|
|
112
|
+
return refreshServedConfig({
|
|
113
|
+
configPath: ctx.actualConfigPath,
|
|
114
|
+
indexName: ctx.indexName,
|
|
115
|
+
store: ctx.store,
|
|
104
116
|
config: ctx.config,
|
|
117
|
+
setConfig: (config) => {
|
|
118
|
+
ctx.config = config;
|
|
119
|
+
},
|
|
120
|
+
invalidateEgressPolicy: async () => {
|
|
121
|
+
const invalidation = await ctx.invalidateEgressPolicy?.();
|
|
122
|
+
if (invalidation) {
|
|
123
|
+
ctx.advanceRequestAuthorizationEpoch?.(invalidation.policyEpoch);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
markContentMutation: () => ctx.markContentMutation?.(),
|
|
127
|
+
markIndexMutation: () => ctx.markIndexMutation?.(),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function service(ctx: ToolContext): Promise<SessionsService> {
|
|
132
|
+
return new SessionsService({
|
|
133
|
+
config: await currentConfig(ctx),
|
|
105
134
|
configPath: ctx.actualConfigPath,
|
|
106
135
|
indexName: ctx.indexName,
|
|
107
136
|
store: ctx.store,
|
|
@@ -120,7 +149,7 @@ export function handleSessionsStatus(ctx: ToolContext): Promise<ToolResult> {
|
|
|
120
149
|
"gno_sessions_status",
|
|
121
150
|
async () => {
|
|
122
151
|
try {
|
|
123
|
-
return await service(ctx).status();
|
|
152
|
+
return await (await service(ctx)).status();
|
|
124
153
|
} catch (error) {
|
|
125
154
|
return rethrowSessionsError(error);
|
|
126
155
|
}
|
|
@@ -146,7 +175,7 @@ export function handleSessionsImport(
|
|
|
146
175
|
try {
|
|
147
176
|
// A child process keeps this server answering during a long import.
|
|
148
177
|
receipt = await importInChildProcess({
|
|
149
|
-
config: ctx
|
|
178
|
+
config: await currentConfig(ctx),
|
|
150
179
|
configPath: ctx.actualConfigPath,
|
|
151
180
|
indexName: ctx.indexName,
|
|
152
181
|
sourceId: args.sourceId,
|
package/src/mcp/tools/status.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { ToolContext } from "../server";
|
|
|
10
10
|
import { buildContentTypeBoostStatus } from "../../config/content-types";
|
|
11
11
|
import { formatChunkingStatus } from "../../core/chunking-status";
|
|
12
12
|
import { OWNER_CONFIG_PATH_FIELDS, withoutFields } from "../../core/host-paths";
|
|
13
|
+
import { formatLinkWorkspace } from "../../core/link-workspace";
|
|
13
14
|
import { formatVectorPartitionLines } from "../../core/vector-partition-status";
|
|
14
15
|
import { resolveModelUri } from "../../llm/registry";
|
|
15
16
|
import { createStandaloneResidentStatus } from "../../serve/resident-status";
|
|
@@ -55,6 +56,8 @@ function formatStatus(status: StatusView): string {
|
|
|
55
56
|
` ${c.name}: ${c.activeDocuments} docs, ${c.totalChunks} chunks` +
|
|
56
57
|
(c.embeddedChunks > 0 ? `, ${c.embeddedChunks} embedded` : "")
|
|
57
58
|
);
|
|
59
|
+
const workspace = formatLinkWorkspace(c);
|
|
60
|
+
if (workspace) lines.push(` Link workspace: ${workspace}`);
|
|
58
61
|
}
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
assertInferenceActive,
|
|
16
16
|
assertInferenceResult,
|
|
17
17
|
inferenceOptions,
|
|
18
|
+
withInferencePage,
|
|
18
19
|
} from "../llm/inference-scope";
|
|
19
20
|
import { ok } from "../store/types";
|
|
20
21
|
|
|
@@ -23,7 +24,6 @@ import { ok } from "../store/types";
|
|
|
23
24
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
25
|
|
|
25
26
|
const EXPANSION_PROMPT_VERSION = "v3";
|
|
26
|
-
const DEFAULT_TIMEOUT_MS = 5000;
|
|
27
27
|
// Non-greedy to avoid matching from first { to last } across multiple objects
|
|
28
28
|
const JSON_EXTRACT_PATTERN = /\{[\s\S]*?\}/;
|
|
29
29
|
const QUOTED_PHRASE_PATTERN = /"([^"]+)"/g;
|
|
@@ -457,8 +457,6 @@ export function parseExpansionOutput(
|
|
|
457
457
|
export interface ExpansionOptions extends InferenceOptions {
|
|
458
458
|
/** Language hint for prompt selection */
|
|
459
459
|
lang?: string;
|
|
460
|
-
/** Timeout in milliseconds */
|
|
461
|
-
timeout?: number;
|
|
462
460
|
/** Optional context that steers expansion for ambiguous queries */
|
|
463
461
|
intent?: string;
|
|
464
462
|
/** Optional bounded context size override for expansion generation */
|
|
@@ -467,59 +465,49 @@ export interface ExpansionOptions extends InferenceOptions {
|
|
|
467
465
|
|
|
468
466
|
/**
|
|
469
467
|
* Expand query using generation model.
|
|
470
|
-
* Returns null
|
|
468
|
+
* Returns null when generation fails or times out, or its output does not
|
|
469
|
+
* parse (graceful degradation).
|
|
471
470
|
*/
|
|
472
471
|
export async function expandQuery(
|
|
473
472
|
genPort: GenerationPort,
|
|
474
473
|
query: string,
|
|
475
474
|
options: ExpansionOptions = {}
|
|
476
475
|
): Promise<StoreResult<ExpansionResult | null>> {
|
|
477
|
-
const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
478
|
-
|
|
479
476
|
// Build prompt
|
|
480
477
|
const prompt = buildExpansionPrompt(query, options);
|
|
481
478
|
|
|
482
479
|
assertInferenceActive(options);
|
|
483
480
|
const operational = inferenceOptions(options);
|
|
484
|
-
const budget = new AbortController();
|
|
485
|
-
const expiresAt = performance.now() + timeout;
|
|
486
|
-
const timer = setTimeout(() => budget.abort(), timeout);
|
|
487
481
|
try {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
482
|
+
// Expansion is best effort but has no budget of its own: slow hardware
|
|
483
|
+
// (CPU generation takes 15-20s) and a cold model load still expand. A
|
|
484
|
+
// generation that hits models.inferenceTimeout fails only expansion.
|
|
485
|
+
const result = await withInferencePage(() =>
|
|
486
|
+
genPort.generate(
|
|
487
|
+
prompt,
|
|
488
|
+
{
|
|
489
|
+
temperature: 0,
|
|
490
|
+
seed: 42,
|
|
491
|
+
maxTokens: 512,
|
|
492
|
+
contextSize: options.contextSize,
|
|
493
|
+
},
|
|
494
|
+
operational
|
|
495
|
+
)
|
|
502
496
|
);
|
|
503
|
-
//
|
|
504
|
-
// cancellation/deadline always wins and cannot become a lexical success.
|
|
497
|
+
// Caller cancellation/deadline always wins and cannot become a lexical success.
|
|
505
498
|
assertInferenceActive(options);
|
|
506
|
-
if (
|
|
507
|
-
return ok(null);
|
|
499
|
+
if (!result) return ok(null);
|
|
508
500
|
assertInferenceResult(result);
|
|
509
501
|
if (!result.ok) return ok(null);
|
|
510
502
|
return ok(parseExpansionOutput(result.value, query));
|
|
511
503
|
} catch (cause) {
|
|
512
504
|
assertInferenceActive(options);
|
|
513
|
-
if (budget.signal.aborted || performance.now() >= expiresAt)
|
|
514
|
-
return ok(null);
|
|
515
505
|
if (
|
|
516
506
|
cause instanceof Error &&
|
|
517
507
|
["AbortError", "TimeoutError"].includes(cause.name)
|
|
518
508
|
)
|
|
519
509
|
throw cause;
|
|
520
510
|
return ok(null);
|
|
521
|
-
} finally {
|
|
522
|
-
clearTimeout(timer);
|
|
523
511
|
}
|
|
524
512
|
}
|
|
525
513
|
|
|
@@ -196,6 +196,7 @@ const loadGraphLinks = async (
|
|
|
196
196
|
seedDocumentIds: number[],
|
|
197
197
|
options: {
|
|
198
198
|
collection?: string;
|
|
199
|
+
collections?: string[];
|
|
199
200
|
includeSimilar?: boolean;
|
|
200
201
|
}
|
|
201
202
|
): Promise<
|
|
@@ -205,6 +206,7 @@ const loadGraphLinks = async (
|
|
|
205
206
|
const neighborsResult = await store.getGraphNeighborsForSeeds({
|
|
206
207
|
seedDocumentIds,
|
|
207
208
|
collection: options.collection,
|
|
209
|
+
collections: options.collections,
|
|
208
210
|
limitEdges: GRAPH_EDGE_LIMIT,
|
|
209
211
|
});
|
|
210
212
|
if (!neighborsResult.ok) {
|
|
@@ -239,6 +241,11 @@ export async function expandGraphCandidates(
|
|
|
239
241
|
fusedCandidates: FusionCandidate[],
|
|
240
242
|
options: {
|
|
241
243
|
collection?: string;
|
|
244
|
+
/**
|
|
245
|
+
* Graph allowlist (plural request scope). Neighbours must belong to one of
|
|
246
|
+
* these collections; defaults to `[collection]` when unset.
|
|
247
|
+
*/
|
|
248
|
+
collections?: string[];
|
|
242
249
|
includeSimilar?: boolean;
|
|
243
250
|
eligibility?: DocumentEligibilityOptions;
|
|
244
251
|
limit?: number;
|
|
@@ -353,8 +360,14 @@ export async function expandGraphCandidates(
|
|
|
353
360
|
}
|
|
354
361
|
|
|
355
362
|
const seedDocumentIds = [...seedByDocid.values()].map(({ doc }) => doc.id);
|
|
363
|
+
const graphAllowlist = options.collections
|
|
364
|
+
? new Set(options.collections)
|
|
365
|
+
: options.collection
|
|
366
|
+
? new Set([options.collection])
|
|
367
|
+
: undefined;
|
|
356
368
|
const linksResult = await loadGraphLinks(store, seedDocumentIds, {
|
|
357
369
|
collection: options.collection,
|
|
370
|
+
collections: options.collections,
|
|
358
371
|
includeSimilar: options.includeSimilar,
|
|
359
372
|
});
|
|
360
373
|
if (!linksResult.ok) {
|
|
@@ -400,9 +413,15 @@ export async function expandGraphCandidates(
|
|
|
400
413
|
return { candidates: [], meta };
|
|
401
414
|
}
|
|
402
415
|
|
|
416
|
+
// A plural allowlist hydrates across its collections; scope is enforced on
|
|
417
|
+
// each neighbour's own collection below, never widened past the allowlist.
|
|
418
|
+
const plural = options.collections !== undefined;
|
|
403
419
|
const docsResult = await store.getDocumentsByDocids(rankedNeighborDocids, {
|
|
404
|
-
eligibility:
|
|
405
|
-
|
|
420
|
+
eligibility:
|
|
421
|
+
plural && options.eligibility
|
|
422
|
+
? { ...options.eligibility, collection: undefined }
|
|
423
|
+
: options.eligibility,
|
|
424
|
+
collection: plural ? undefined : options.collection,
|
|
406
425
|
activeOnly: true,
|
|
407
426
|
});
|
|
408
427
|
if (!docsResult.ok) {
|
|
@@ -413,6 +432,7 @@ export async function expandGraphCandidates(
|
|
|
413
432
|
const metadataFilteredDocs = docsResult.value.filter(
|
|
414
433
|
(doc) =>
|
|
415
434
|
doc.mirrorHash &&
|
|
435
|
+
(!graphAllowlist || graphAllowlist.has(doc.collection)) &&
|
|
416
436
|
(options.relPathPrefix === undefined ||
|
|
417
437
|
sourceRelPath(doc) === options.relPathPrefix ||
|
|
418
438
|
sourceRelPath(doc).startsWith(`${options.relPathPrefix}/`)) &&
|
package/src/pipeline/hybrid.ts
CHANGED
|
@@ -520,7 +520,6 @@ async function searchHybridWithHydration(
|
|
|
520
520
|
const expandResult = await expandQuery(expandPort, query, {
|
|
521
521
|
// Use queryLanguage for prompt selection, NOT options.lang (retrieval filter)
|
|
522
522
|
lang: queryLanguage,
|
|
523
|
-
timeout: pipelineConfig.expansionTimeout,
|
|
524
523
|
intent: options.intent,
|
|
525
524
|
contextSize: deps.config.models?.expandContextSize,
|
|
526
525
|
});
|
|
@@ -804,6 +803,7 @@ async function searchHybridWithHydration(
|
|
|
804
803
|
fusedCandidates,
|
|
805
804
|
{
|
|
806
805
|
collection: options.collection,
|
|
806
|
+
collections: options.graphCollections,
|
|
807
807
|
includeSimilar: vectorAvailable,
|
|
808
808
|
eligibility: vectorEligibility,
|
|
809
809
|
limit,
|
package/src/pipeline/types.ts
CHANGED
|
@@ -191,6 +191,12 @@ export interface SearchOptions extends InferenceOptions {
|
|
|
191
191
|
minScore?: number;
|
|
192
192
|
/** Filter by collection */
|
|
193
193
|
collection?: string;
|
|
194
|
+
/**
|
|
195
|
+
* Internal graph allowlist for a request partitioned into one retrieval per
|
|
196
|
+
* collection (Context Capsules, replay): graph neighbours may come from any
|
|
197
|
+
* of these collections, never from others. Defaults to `[collection]`.
|
|
198
|
+
*/
|
|
199
|
+
graphCollections?: string[];
|
|
194
200
|
/** Internal exact corpus scope used by deterministic retrieval replay. */
|
|
195
201
|
retrievalScope?: {
|
|
196
202
|
relPathPrefix?: string;
|
|
@@ -406,8 +412,6 @@ export type RerankedCandidate = FusionCandidate & {
|
|
|
406
412
|
|
|
407
413
|
/** Search pipeline configuration */
|
|
408
414
|
export interface PipelineConfig {
|
|
409
|
-
/** Expansion timeout in ms */
|
|
410
|
-
expansionTimeout: number;
|
|
411
415
|
/** Max candidates to rerank */
|
|
412
416
|
rerankCandidates: number;
|
|
413
417
|
/** RRF configuration */
|
|
@@ -418,7 +422,6 @@ export interface PipelineConfig {
|
|
|
418
422
|
|
|
419
423
|
/** Default pipeline configuration */
|
|
420
424
|
export const DEFAULT_PIPELINE_CONFIG: PipelineConfig = {
|
|
421
|
-
expansionTimeout: 5000,
|
|
422
425
|
rerankCandidates: 20,
|
|
423
426
|
rrf: DEFAULT_RRF_CONFIG,
|
|
424
427
|
blendingSchedule: DEFAULT_BLENDING_SCHEDULE,
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
} from "../core/findings-run-state";
|
|
31
31
|
import { acquireCliWriteLease } from "../core/write-lease";
|
|
32
32
|
|
|
33
|
-
/** Audit report cap
|
|
33
|
+
/** Audit report cap for the daemon findings pass. */
|
|
34
34
|
const FINDINGS_AUDIT_MAX_FINDINGS = 1000;
|
|
35
35
|
const LEASE_HOLDER_COMMAND = "gno daemon (findings pass)";
|
|
36
36
|
const CONTROL_CHARS = /\p{Cc}/gu;
|
|
@@ -79,7 +79,12 @@ export async function handleImpact(
|
|
|
79
79
|
): Promise<Response> {
|
|
80
80
|
const ref = url.searchParams.get("ref")?.trim();
|
|
81
81
|
if (!ref) return errorResponse("VALIDATION", "ref is required");
|
|
82
|
-
const
|
|
82
|
+
const collections = url.searchParams
|
|
83
|
+
.getAll("collection")
|
|
84
|
+
.map((value) => value.trim())
|
|
85
|
+
.filter(Boolean);
|
|
86
|
+
const input: KnowledgeImpactInput =
|
|
87
|
+
collections.length > 0 ? { collections } : {};
|
|
83
88
|
for (const [queryName, inputName] of [
|
|
84
89
|
["maxDepth", "maxDepth"],
|
|
85
90
|
["maxNodes", "maxNodes"],
|
|
@@ -34,6 +34,8 @@ export interface LinkResponse {
|
|
|
34
34
|
resolvedUri?: string;
|
|
35
35
|
/** Resolved target title (if found) */
|
|
36
36
|
resolvedTitle?: string;
|
|
37
|
+
/** Collection of the resolved target (may differ from the source's) */
|
|
38
|
+
resolvedCollection?: string;
|
|
37
39
|
}>;
|
|
38
40
|
meta: {
|
|
39
41
|
docid: string;
|
|
@@ -49,6 +51,8 @@ export interface BacklinkResponse {
|
|
|
49
51
|
sourceDocid: string;
|
|
50
52
|
sourceUri: string;
|
|
51
53
|
sourceTitle?: string;
|
|
54
|
+
/** Collection of the linking document */
|
|
55
|
+
sourceCollection?: string;
|
|
52
56
|
linkText?: string;
|
|
53
57
|
startLine: number;
|
|
54
58
|
startCol: number;
|
|
@@ -189,6 +193,11 @@ export async function handleDocLinks(
|
|
|
189
193
|
targetRefNorm: l.targetRefNorm,
|
|
190
194
|
targetCollection: l.targetCollection || doc.collection,
|
|
191
195
|
linkType: l.linkType,
|
|
196
|
+
source: {
|
|
197
|
+
collection: doc.collection,
|
|
198
|
+
relPath: doc.relPath,
|
|
199
|
+
explicit: Boolean(l.targetCollection),
|
|
200
|
+
},
|
|
192
201
|
}))
|
|
193
202
|
);
|
|
194
203
|
const resolutionAvailable = resolvedResult.ok;
|
|
@@ -217,6 +226,9 @@ export async function handleDocLinks(
|
|
|
217
226
|
resolvedDocid: resolved.docid,
|
|
218
227
|
resolvedUri: resolved.uri,
|
|
219
228
|
resolvedTitle: resolved.title ?? undefined,
|
|
229
|
+
...(resolved.collection && {
|
|
230
|
+
resolvedCollection: resolved.collection,
|
|
231
|
+
}),
|
|
220
232
|
}),
|
|
221
233
|
}),
|
|
222
234
|
};
|
|
@@ -273,6 +285,7 @@ export async function handleDocBacklinks(
|
|
|
273
285
|
backlinks: backlinks.map((b) => ({
|
|
274
286
|
sourceDocid: b.sourceDocid,
|
|
275
287
|
sourceUri: b.sourceDocUri,
|
|
288
|
+
...(b.sourceCollection && { sourceCollection: b.sourceCollection }),
|
|
276
289
|
...(b.sourceDocTitle && { sourceTitle: b.sourceDocTitle }),
|
|
277
290
|
...(b.linkText && { linkText: b.linkText }),
|
|
278
291
|
startLine: b.startLine,
|
|
@@ -16,7 +16,6 @@ import type { SqliteAdapter } from "../../store/sqlite/adapter";
|
|
|
16
16
|
import type { RequestPeerServer } from "../request-locality";
|
|
17
17
|
import type { ContextHolder } from "./api";
|
|
18
18
|
|
|
19
|
-
import { getIndexDbPath } from "../../app/constants";
|
|
20
19
|
import { getConfigPaths, loadConfig } from "../../config";
|
|
21
20
|
import { withContentTypeRules } from "../../ingestion";
|
|
22
21
|
import {
|
|
@@ -28,8 +27,14 @@ import {
|
|
|
28
27
|
runAutomationProfile,
|
|
29
28
|
setAutomationProfile,
|
|
30
29
|
} from "../../sessions/automation";
|
|
31
|
-
import { assertSessionBinding } from "../../sessions/binding";
|
|
32
30
|
import { SessionSourceSchema, watchedCollections } from "../../sessions/config";
|
|
31
|
+
import {
|
|
32
|
+
adoptServedConfig,
|
|
33
|
+
assertInstanceBinding as assertConfigBinding,
|
|
34
|
+
readInstanceConfig,
|
|
35
|
+
refreshServedConfig,
|
|
36
|
+
type ServedSessionsConfig,
|
|
37
|
+
} from "../../sessions/config-refresh";
|
|
33
38
|
import { importInChildProcess } from "../../sessions/import-child";
|
|
34
39
|
import { SessionsService } from "../../sessions/service";
|
|
35
40
|
import {
|
|
@@ -186,18 +191,11 @@ function instanceIdentity(ctxHolder: ContextHolder): {
|
|
|
186
191
|
}
|
|
187
192
|
|
|
188
193
|
/** Refuse an archive config opened against a different index (and vice versa). */
|
|
189
|
-
|
|
194
|
+
function assertInstanceBinding(
|
|
190
195
|
ctxHolder: ContextHolder,
|
|
191
196
|
config: Config = ctxHolder.config
|
|
192
197
|
): Promise<void> {
|
|
193
|
-
|
|
194
|
-
if (!config.sessions) return;
|
|
195
|
-
await assertSessionBinding({
|
|
196
|
-
config,
|
|
197
|
-
configPath,
|
|
198
|
-
indexName,
|
|
199
|
-
dbPath: getIndexDbPath(indexName),
|
|
200
|
-
});
|
|
198
|
+
return assertConfigBinding(instanceIdentity(ctxHolder), config);
|
|
201
199
|
}
|
|
202
200
|
|
|
203
201
|
/** Service over the instance's own config/index pair, binding checked. */
|
|
@@ -215,55 +213,47 @@ async function archiveService(
|
|
|
215
213
|
});
|
|
216
214
|
}
|
|
217
215
|
|
|
216
|
+
/** This instance's served config, as the shared config refresh sees it. */
|
|
217
|
+
function servedConfig(
|
|
218
|
+
ctxHolder: ContextHolder,
|
|
219
|
+
store: SqliteAdapter
|
|
220
|
+
): ServedSessionsConfig {
|
|
221
|
+
return {
|
|
222
|
+
...instanceIdentity(ctxHolder),
|
|
223
|
+
store,
|
|
224
|
+
config: ctxHolder.config,
|
|
225
|
+
setConfig: (config) => {
|
|
226
|
+
ctxHolder.config = config;
|
|
227
|
+
ctxHolder.current = { ...ctxHolder.current, config };
|
|
228
|
+
ctxHolder.watchService?.updateCollections(
|
|
229
|
+
watchedCollections(config),
|
|
230
|
+
withContentTypeRules({}, config)
|
|
231
|
+
);
|
|
232
|
+
},
|
|
233
|
+
invalidateEgressPolicy: async () => {
|
|
234
|
+
await ctxHolder.invalidateEgressPolicy?.();
|
|
235
|
+
},
|
|
236
|
+
markContentMutation: () => ctxHolder.markContentMutation?.(),
|
|
237
|
+
markIndexMutation: () => ctxHolder.markIndexMutation?.(),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
218
241
|
/**
|
|
219
242
|
* Adopt a config the sessions service already persisted: project collections
|
|
220
243
|
* and contexts into the open store, swap the in-memory context, and refresh
|
|
221
|
-
* the watcher, egress policy and mutation generations
|
|
222
|
-
* config-sync route helpers).
|
|
244
|
+
* the watcher, egress policy and mutation generations.
|
|
223
245
|
*/
|
|
224
|
-
|
|
246
|
+
function adoptConfig(
|
|
225
247
|
ctxHolder: ContextHolder,
|
|
226
248
|
store: SqliteAdapter,
|
|
227
249
|
config: Config
|
|
228
250
|
): Promise<void> {
|
|
229
|
-
|
|
230
|
-
if (!collections.ok) {
|
|
231
|
-
throw new Error(
|
|
232
|
-
`Config saved but collection sync failed: ${collections.error.message}`
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
const contexts = await store.syncContexts(config.contexts ?? []);
|
|
236
|
-
if (!contexts.ok) {
|
|
237
|
-
throw new Error(
|
|
238
|
-
`Config saved but context sync failed: ${contexts.error.message}`
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
ctxHolder.config = config;
|
|
242
|
-
ctxHolder.current = { ...ctxHolder.current, config };
|
|
243
|
-
ctxHolder.watchService?.updateCollections(
|
|
244
|
-
watchedCollections(config),
|
|
245
|
-
withContentTypeRules({}, config)
|
|
246
|
-
);
|
|
247
|
-
await ctxHolder.invalidateEgressPolicy?.();
|
|
248
|
-
ctxHolder.markContentMutation?.();
|
|
249
|
-
ctxHolder.markIndexMutation?.();
|
|
251
|
+
return adoptServedConfig(servedConfig(ctxHolder, store), config);
|
|
250
252
|
}
|
|
251
253
|
|
|
252
|
-
/**
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
* before it can touch this one.
|
|
256
|
-
*/
|
|
257
|
-
async function readConfigFile(ctxHolder: ContextHolder): Promise<Config> {
|
|
258
|
-
const loaded = await loadConfig(instanceIdentity(ctxHolder).configPath);
|
|
259
|
-
if (!loaded.ok) {
|
|
260
|
-
throw new SessionsError(
|
|
261
|
-
"SESSIONS_RUNTIME_FAILURE",
|
|
262
|
-
"The server could not read its config file; fix the file (gno doctor shows the error) and reload."
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
await assertInstanceBinding(ctxHolder, loaded.value);
|
|
266
|
-
return loaded.value;
|
|
254
|
+
/** Read this instance's config file, binding checked (see readInstanceConfig). */
|
|
255
|
+
function readConfigFile(ctxHolder: ContextHolder): Promise<Config> {
|
|
256
|
+
return readInstanceConfig(instanceIdentity(ctxHolder));
|
|
267
257
|
}
|
|
268
258
|
|
|
269
259
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -282,9 +272,7 @@ export async function refreshSessionsConfig(
|
|
|
282
272
|
store: SqliteAdapter
|
|
283
273
|
): Promise<Response | null> {
|
|
284
274
|
try {
|
|
285
|
-
|
|
286
|
-
if (Bun.deepEquals(config, ctxHolder.config)) return null;
|
|
287
|
-
await adoptConfig(ctxHolder, store, config);
|
|
275
|
+
await refreshServedConfig(servedConfig(ctxHolder, store));
|
|
288
276
|
return null;
|
|
289
277
|
} catch (error) {
|
|
290
278
|
return sessionsErrorResponse(error);
|