@oscharko-dev/keiko-server 0.2.6 → 0.2.8
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/dist/.tsbuildinfo +1 -1
- package/dist/chat-compaction-evidence.d.ts +12 -0
- package/dist/chat-compaction-evidence.d.ts.map +1 -0
- package/dist/chat-compaction-evidence.js +46 -0
- package/dist/chat-handlers.d.ts +16 -0
- package/dist/chat-handlers.d.ts.map +1 -1
- package/dist/chat-handlers.js +78 -28
- package/dist/chat-stream-handlers.d.ts.map +1 -1
- package/dist/chat-stream-handlers.js +13 -1
- package/dist/conversation-compaction.d.ts +12 -0
- package/dist/conversation-compaction.d.ts.map +1 -0
- package/dist/conversation-compaction.js +102 -0
- package/dist/deps.d.ts +3 -0
- package/dist/deps.d.ts.map +1 -1
- package/dist/deps.js +3 -2
- package/dist/files.d.ts +18 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +174 -0
- package/dist/gateway-readiness.d.ts +6 -0
- package/dist/gateway-readiness.d.ts.map +1 -0
- package/dist/gateway-readiness.js +624 -0
- package/dist/grounded-context-diagnostics.d.ts +5 -0
- package/dist/grounded-context-diagnostics.d.ts.map +1 -0
- package/dist/grounded-context-diagnostics.js +77 -0
- package/dist/grounded-orchestrator.d.ts +2 -0
- package/dist/grounded-orchestrator.d.ts.map +1 -1
- package/dist/grounded-orchestrator.js +122 -53
- package/dist/grounded-qa-hybrid.d.ts.map +1 -1
- package/dist/grounded-qa-hybrid.js +7 -4
- package/dist/grounded-qa-multi-source.d.ts.map +1 -1
- package/dist/grounded-qa-multi-source.js +49 -2
- package/dist/grounded-qa.d.ts +4 -0
- package/dist/grounded-qa.d.ts.map +1 -1
- package/dist/grounded-qa.js +36 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/local-knowledge-grounded-qa.d.ts.map +1 -1
- package/dist/local-knowledge-grounded-qa.js +11 -2
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +5 -10
- package/dist/run-handlers.d.ts +0 -1
- package/dist/run-handlers.d.ts.map +1 -1
- package/dist/run-handlers.js +0 -217
- package/dist/store/db.d.ts.map +1 -1
- package/dist/store/db.js +2 -1
- package/dist/store/index.d.ts +1 -1
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/messages.d.ts +2 -1
- package/dist/store/messages.d.ts.map +1 -1
- package/dist/store/messages.js +46 -4
- package/dist/store/schema.d.ts +1 -1
- package/dist/store/schema.d.ts.map +1 -1
- package/dist/store/schema.js +7 -1
- package/dist/store/types.d.ts +3 -2
- package/dist/store/types.d.ts.map +1 -1
- package/package.json +19 -19
- package/dist/grounded-handoff.d.ts +0 -4
- package/dist/grounded-handoff.d.ts.map +0 -1
- package/dist/grounded-handoff.js +0 -445
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// PR4-W1 grounded diagnostics observer (ADR-0055 D1). A PURE, no-IO, no-clock function that
|
|
2
|
+
// accepts a fully assembled `ConnectedContextPack` and a `ContextProfile` and returns a pack
|
|
3
|
+
// whose `diagnostics.contextBudget?` is populated with a deterministic `ContextBudget` derived by
|
|
4
|
+
// running the keiko-workflows allocator over lanes mapped from the pack.
|
|
5
|
+
//
|
|
6
|
+
// NON-NEGOTIABLE INVARIANT (AC5): this observer MUST NOT touch any field a prompt builder reads.
|
|
7
|
+
// `buildGroundedGatewayMessages` (grounded-qa.ts) reads only pack.schemaVersion / stableId /
|
|
8
|
+
// scope / query / budget / usage / omitted / files / uncertainty — never pack.diagnostics. The
|
|
9
|
+
// observer therefore changes ONLY the additive optional `diagnostics.contextBudget?` field while
|
|
10
|
+
// preserving the existing `diagnostics.rankedCandidates` (the first-lexical-ring explainability).
|
|
11
|
+
// pack.files, pack.budget, pack.usage and pack.stableId are returned reference-identical.
|
|
12
|
+
import { CONTEXT_ENGINEERING_SCHEMA_VERSION, } from "@oscharko-dev/keiko-contracts";
|
|
13
|
+
import { allocateContext, DEFAULT_CONTEXT_BUDGET, } from "@oscharko-dev/keiko-workflows";
|
|
14
|
+
// The grounded path carries repository evidence excerpts only; the system-contract / user-task /
|
|
15
|
+
// plan / tool / memory / history / verification lanes are not assembled here, so they are passed
|
|
16
|
+
// empty. Diagnostics over the populated repo-evidence lane are the W1 deliverable.
|
|
17
|
+
function repoEvidenceLaneItems(pack) {
|
|
18
|
+
const items = [];
|
|
19
|
+
for (const file of pack.files) {
|
|
20
|
+
for (const excerpt of file.excerpts) {
|
|
21
|
+
items.push({
|
|
22
|
+
id: excerpt.atom.stableId,
|
|
23
|
+
text: excerpt.content,
|
|
24
|
+
score: excerpt.atom.score,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return items;
|
|
29
|
+
}
|
|
30
|
+
function groundedLanes(pack) {
|
|
31
|
+
return [{ laneId: "repo-evidence", items: repoEvidenceLaneItems(pack) }];
|
|
32
|
+
}
|
|
33
|
+
// Builds a ContextBudget whose `profile` is the supplied profile (so validateContextBudget's
|
|
34
|
+
// profile-identity check holds) while reusing the canonical, tunable lane rows. Pure — no clock,
|
|
35
|
+
// no IO, never mutates the inputs.
|
|
36
|
+
function budgetForProfile(profile) {
|
|
37
|
+
return {
|
|
38
|
+
schemaVersion: CONTEXT_ENGINEERING_SCHEMA_VERSION,
|
|
39
|
+
profile,
|
|
40
|
+
lanes: DEFAULT_CONTEXT_BUDGET.lanes,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function diagnosticsWithBudget(existing, budget) {
|
|
44
|
+
const rankedCandidates = existing?.rankedCandidates ?? [];
|
|
45
|
+
return { ...existing, rankedCandidates, contextBudget: budget };
|
|
46
|
+
}
|
|
47
|
+
// SHARED lane-derivation + allocation pass (single source of truth for the observer AND the
|
|
48
|
+
// evidence producer below). Pure, no-IO, no-clock: maps the pack's repo-evidence lane, builds the
|
|
49
|
+
// profile budget, and runs the deterministic allocator once. Both the BFF/UI ContextBudget slot
|
|
50
|
+
// and the regulated ContextAssemblyDiagnostics are projected from THIS result — there is no
|
|
51
|
+
// divergent derivation path that could let the two views disagree for the same pack + profile.
|
|
52
|
+
function allocateGroundedContext(pack, profile) {
|
|
53
|
+
const budget = budgetForProfile(profile);
|
|
54
|
+
const result = allocateContext({ profile, budget, lanes: groundedLanes(pack) });
|
|
55
|
+
return { budget, result };
|
|
56
|
+
}
|
|
57
|
+
// EVIDENCE producer (ADR-0056 W3). Returns the rich ContextAssemblyDiagnostics
|
|
58
|
+
// (allocateContext(...).diagnostics) for the pack's repo-evidence lane under the supplied profile.
|
|
59
|
+
// This is the value persisted to EvidenceManifest.contextAssembly? — NOT the ContextBudget that
|
|
60
|
+
// ContextPackDiagnostics.contextBudget? carries. Derives the lane via the same shared helper the
|
|
61
|
+
// observer uses, so the persisted diagnostics describe exactly the budget plan the observer
|
|
62
|
+
// attached. Pure and deterministic.
|
|
63
|
+
export function deriveGroundedContextAssembly(pack, profile) {
|
|
64
|
+
return allocateGroundedContext(pack, profile).result.diagnostics;
|
|
65
|
+
}
|
|
66
|
+
// Runs the deterministic allocator over the pack-derived lanes and attaches the resulting
|
|
67
|
+
// ContextBudget to pack.diagnostics.contextBudget. The allocator pass exercises the budget plan
|
|
68
|
+
// over real lane items (proving the budget is meaningful for this pack); the attached value is the
|
|
69
|
+
// ContextBudget that ContextPackDiagnostics.contextBudget? carries (the only diagnostics slot the
|
|
70
|
+
// pack contract exposes). All other pack fields are returned unchanged.
|
|
71
|
+
export function attachContextBudgetDiagnostics(pack, profile) {
|
|
72
|
+
const { budget } = allocateGroundedContext(pack, profile);
|
|
73
|
+
return {
|
|
74
|
+
...pack,
|
|
75
|
+
diagnostics: diagnosticsWithBudget(pack.diagnostics, budget),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ConnectedContextPack, type ExplorationBudget, type RetrievalQuery, type SelectedScope } from "@oscharko-dev/keiko-contracts/connected-context";
|
|
2
|
+
import type { ContextProfile } from "@oscharko-dev/keiko-contracts";
|
|
2
3
|
import { type ClarificationPrompt, type ExplorationPlan, type MicroIndex } from "@oscharko-dev/keiko-workflows";
|
|
3
4
|
import { DEFAULT_SEARCH_LIMITS, type WorkspaceFs, type WorkspaceInfo } from "@oscharko-dev/keiko-workspace";
|
|
4
5
|
import { type GroundedAnswerPayload } from "./grounded-answer.js";
|
|
@@ -19,6 +20,7 @@ export interface OrchestratorDeps {
|
|
|
19
20
|
readonly detectWorkspace?: (root: string, fs: WorkspaceFs) => WorkspaceInfo;
|
|
20
21
|
readonly recordPlan?: (plan: ExplorationPlan) => void;
|
|
21
22
|
readonly microIndex?: MicroIndex;
|
|
23
|
+
readonly contextProfile?: ContextProfile | undefined;
|
|
22
24
|
}
|
|
23
25
|
export interface OrchestratorOutput {
|
|
24
26
|
readonly pack: ConnectedContextPack;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grounded-orchestrator.d.ts","sourceRoot":"","sources":["../src/grounded-orchestrator.ts"],"names":[],"mappings":"AAYA,OAAO,EAGL,KAAK,oBAAoB,
|
|
1
|
+
{"version":3,"file":"grounded-orchestrator.d.ts","sourceRoot":"","sources":["../src/grounded-orchestrator.ts"],"names":[],"mappings":"AAYA,OAAO,EAGL,KAAK,oBAAoB,EAGzB,KAAK,iBAAiB,EAGtB,KAAK,cAAc,EACnB,KAAK,aAAa,EAEnB,MAAM,iDAAiD,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAUL,KAAK,mBAAmB,EAExB,KAAK,eAAe,EAEpB,KAAK,UAAU,EAIhB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAEL,qBAAqB,EAiBrB,KAAK,WAAW,EAChB,KAAK,aAAa,EAGnB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAkC,KAAK,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAUlG,MAAM,WAAW,gBAAgB;IAG/B,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACtF;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,iBAAiB,CAAC;CACrC;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAE1C,QAAQ,CAAC,EAAE,CAAC,EAAE,WAAW,CAAC;IAE1B,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,KAAK,aAAa,CAAC;IAE5E,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAC;IAEtD,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;IAKjC,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;CACtD;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE,eAAe,CAAC;CACjC;AAMD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;CAChC;AAKD,qBAAa,wBAAyB,SAAQ,KAAK;aACd,aAAa,EAAE,mBAAmB;gBAAlC,aAAa,EAAE,mBAAmB;CAItE;AAMD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,wBAAwB,GAAG,MAAM,CAgBhF;AAk8BD,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAM/E;AA09BD,wBAAsB,4BAA4B,CAChD,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,mBAAmB,CAAC,CA4C9B;AAED,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,kBAAkB,CAAC,CA8B7B;AAID,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
|
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
12
|
import { isValidScopePath, } from "@oscharko-dev/keiko-contracts/connected-context";
|
|
13
13
|
import { advanceRing, applyUsage, assembleContextPack, canContinue, classifyRetrievalIntent, complete, contextPackIndexKey, planAndGovern, rankCandidates, } from "@oscharko-dev/keiko-workflows";
|
|
14
|
-
import { DEFAULT_SEARCH_LIMITS, FileTooLargeError, RepoSearchUnsupportedFileError, detectWorkspaceAt, findFiles, gitHistoryAdapter, importGraphAdapter, readExcerpt, resolveWithinWorkspace, runStructuralAdapters, searchText, testSourcePairingAdapter, containedRealPathInfo, evidenceAtomStableId, } from "@oscharko-dev/keiko-workspace";
|
|
14
|
+
import { CANONICAL_MANIFEST_BASENAMES, DEFAULT_SEARCH_LIMITS, FileTooLargeError, RepoSearchUnsupportedFileError, detectWorkspaceAt, findFiles, gitHistoryAdapter, importGraphAdapter, isCanonicalMetadataFile, isDenied, readExcerpt, resolveWithinWorkspace, runStructuralAdapters, searchText, testSourcePairingAdapter, containedRealPathInfo, evidenceAtomStableId, } from "@oscharko-dev/keiko-workspace";
|
|
15
15
|
import { CancelledError } from "@oscharko-dev/keiko-model-gateway";
|
|
16
16
|
import { nodeWorkspaceFs } from "@oscharko-dev/keiko-workspace/internal/fs";
|
|
17
17
|
import { normalizeGroundedAnswerPayload } from "./grounded-answer.js";
|
|
18
18
|
import { collectConnectedDocumentEvidence, isConnectedDocumentPath, } from "./grounded-document-evidence.js";
|
|
19
|
+
import { attachContextBudgetDiagnostics } from "./grounded-context-diagnostics.js";
|
|
19
20
|
// Raised when the planner asks for clarification (no anchors, too-generic prompt, etc.). The
|
|
20
21
|
// route maps this to a 400 BAD_REQUEST via clarificationUserMessage below; the Error message
|
|
21
22
|
// itself keeps the stable machine-ish form for logs and tests.
|
|
@@ -45,6 +46,23 @@ export function clarificationUserMessage(error) {
|
|
|
45
46
|
const exampleText = examples.length > 0 ? ` Zum Beispiel: ${examples.map((q) => `"${q}"`).join(" oder ")}` : "";
|
|
46
47
|
return `${intro}${anchorHint}${exampleText}`;
|
|
47
48
|
}
|
|
49
|
+
// Maps the workspace-layer SearchDiagnostics.rankedCandidates onto the contract pack-diagnostics
|
|
50
|
+
// shape. Structurally identical (path/bucket/score/ecosystem/signals) but mapped explicitly so the
|
|
51
|
+
// workspace and contracts types stay decoupled. Returns undefined when no diagnostics are present.
|
|
52
|
+
function toPackDiagnostics(diagnostics) {
|
|
53
|
+
if (diagnostics === undefined) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
rankedCandidates: diagnostics.rankedCandidates.map((entry) => ({
|
|
58
|
+
scopePath: entry.scopePath,
|
|
59
|
+
bucket: entry.bucket,
|
|
60
|
+
score: entry.score,
|
|
61
|
+
ecosystem: entry.ecosystem,
|
|
62
|
+
signals: entry.signals.map((signal) => ({ name: signal.name, value: signal.value })),
|
|
63
|
+
})),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
48
66
|
const TEXT_ENCODER = new TextEncoder();
|
|
49
67
|
function throwIfCancelled(signal) {
|
|
50
68
|
if (signal?.aborted === true) {
|
|
@@ -245,6 +263,7 @@ async function runRing(ring, inputs) {
|
|
|
245
263
|
omitted: omittedFromSearchCandidates(result.candidates, inputs.nowMs()),
|
|
246
264
|
uncertainty: [],
|
|
247
265
|
usage: usageDelta({ elapsedMs: result.elapsedMs }),
|
|
266
|
+
diagnostics: toPackDiagnostics(result.diagnostics),
|
|
248
267
|
};
|
|
249
268
|
}
|
|
250
269
|
// Keep the planner's ring split authoritative: the structural ring should only run the
|
|
@@ -292,6 +311,9 @@ async function runAllRings(rings, inputs, initialGovernor) {
|
|
|
292
311
|
const atoms = [];
|
|
293
312
|
const omitted = [];
|
|
294
313
|
const uncertainty = [];
|
|
314
|
+
// Ring order is fixed by the plan, so capturing the first lexical ring's diagnostics is
|
|
315
|
+
// deterministic. (There is normally exactly one lexical ring.)
|
|
316
|
+
let diagnostics;
|
|
295
317
|
let governor = initialGovernor;
|
|
296
318
|
for (const ring of rings) {
|
|
297
319
|
throwIfCancelled(inputs.signal);
|
|
@@ -306,6 +328,8 @@ async function runAllRings(rings, inputs, initialGovernor) {
|
|
|
306
328
|
}
|
|
307
329
|
const result = await runRing(ring, inputs);
|
|
308
330
|
throwIfCancelled(inputs.signal);
|
|
331
|
+
// First lexical ring wins (??= never overwrites once set); ring order is plan-fixed.
|
|
332
|
+
diagnostics ??= result.diagnostics;
|
|
309
333
|
const afterRing = applyUsage(governor, result.usage);
|
|
310
334
|
atoms.push(...result.atoms);
|
|
311
335
|
omitted.push(...result.omitted);
|
|
@@ -320,7 +344,7 @@ async function runAllRings(rings, inputs, initialGovernor) {
|
|
|
320
344
|
if (governor.status === "running") {
|
|
321
345
|
governor = complete(governor);
|
|
322
346
|
}
|
|
323
|
-
return { atoms, omitted, governor, uncertainty };
|
|
347
|
+
return { atoms, omitted, governor, uncertainty, diagnostics };
|
|
324
348
|
}
|
|
325
349
|
const DEFAULT_EXCERPT_WINDOW = { startLine: 1, endLine: 200 };
|
|
326
350
|
const EXCERPT_CONTEXT_LINES = 2;
|
|
@@ -366,38 +390,21 @@ const PROJECT_METADATA_QUERY_TERMS = [
|
|
|
366
390
|
"vitest",
|
|
367
391
|
"yarn",
|
|
368
392
|
];
|
|
369
|
-
|
|
370
|
-
|
|
393
|
+
// Dependency lockfiles surfaced for project-metadata questions (unchanged behaviour). The manifest
|
|
394
|
+
// basenames themselves now come from the shared ecosystem registry (CANONICAL_MANIFEST_BASENAMES),
|
|
395
|
+
// which is a superset of the prior JS/TS-only list and additionally covers Maven/Gradle/Go/Rust/
|
|
396
|
+
// Python/.NET/etc., so "Which Java version does this project use?" injects pom.xml/build.gradle as
|
|
397
|
+
// deterministic score-1 metadata atoms.
|
|
398
|
+
const PROJECT_METADATA_LOCKFILES = [
|
|
371
399
|
"package-lock.json",
|
|
372
400
|
"pnpm-lock.yaml",
|
|
373
401
|
"yarn.lock",
|
|
374
402
|
"bun.lock",
|
|
375
403
|
"bun.lockb",
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
"vitest.setup.ts",
|
|
381
|
-
"vite.config.ts",
|
|
382
|
-
"vite.config.mts",
|
|
383
|
-
"vite.config.js",
|
|
384
|
-
"vite.config.mjs",
|
|
385
|
-
"jest.config.ts",
|
|
386
|
-
"jest.config.js",
|
|
387
|
-
"jest.config.mjs",
|
|
388
|
-
"playwright.config.ts",
|
|
389
|
-
"playwright.config.js",
|
|
390
|
-
"cypress.config.ts",
|
|
391
|
-
"cypress.config.js",
|
|
392
|
-
"next.config.ts",
|
|
393
|
-
"next.config.js",
|
|
394
|
-
"next.config.mjs",
|
|
395
|
-
"tsconfig.json",
|
|
396
|
-
"eslint.config.ts",
|
|
397
|
-
"eslint.config.js",
|
|
398
|
-
"eslint.config.mjs",
|
|
399
|
-
"postcss.config.js",
|
|
400
|
-
"postcss.config.mjs",
|
|
404
|
+
];
|
|
405
|
+
const PROJECT_METADATA_FILENAMES = [
|
|
406
|
+
...CANONICAL_MANIFEST_BASENAMES,
|
|
407
|
+
...PROJECT_METADATA_LOCKFILES,
|
|
401
408
|
];
|
|
402
409
|
const REPOSITORY_OVERVIEW_FILENAMES = [
|
|
403
410
|
"README.md",
|
|
@@ -572,16 +579,32 @@ function safeReadDir(searchScope, fs, scopePath) {
|
|
|
572
579
|
return [];
|
|
573
580
|
}
|
|
574
581
|
}
|
|
582
|
+
// Bound on how many service subdirectories under a `dir/*` pattern are scanned, so a monorepo with
|
|
583
|
+
// thousands of packages cannot trigger an unbounded directory fan-out (the per-result cap in the
|
|
584
|
+
// caller is MAX_WORKSPACE_MANIFESTS; this caps the WORK, not just the output).
|
|
585
|
+
const MAX_MONOREPO_SERVICE_DIRS = 96;
|
|
586
|
+
// Canonical project manifests of ANY ecosystem present directly inside `dir` (one bounded readDir,
|
|
587
|
+
// realpath-contained, no symlink following). Replaces the prior package.json-only probe so a
|
|
588
|
+
// polyglot monorepo surfaces service-local pom.xml / go.mod / Cargo.toml / *.csproj, not just
|
|
589
|
+
// JS packages. isDenied is applied even though the registry is deny-clean (defence in depth), and
|
|
590
|
+
// the result is sorted for deterministic evidence ordering.
|
|
591
|
+
function canonicalManifestScopePathsInDir(dir, searchScope, fs) {
|
|
592
|
+
return safeReadDir(searchScope, fs, dir)
|
|
593
|
+
.filter((entry) => !entry.isDirectory && !entry.isSymbolicLink)
|
|
594
|
+
.map((entry) => joinScopePath(dir, entry.name))
|
|
595
|
+
.filter((scopePath) => isCanonicalMetadataFile(scopePath) && !isDenied(scopePath))
|
|
596
|
+
.sort();
|
|
597
|
+
}
|
|
575
598
|
function expandWorkspacePattern(pattern, searchScope, fs) {
|
|
576
599
|
const normalized = normalizeWorkspacePattern(pattern);
|
|
577
600
|
if (normalized === undefined) {
|
|
578
601
|
return [];
|
|
579
602
|
}
|
|
580
603
|
if (!normalized.includes("*")) {
|
|
581
|
-
const
|
|
582
|
-
? normalized
|
|
583
|
-
:
|
|
584
|
-
return
|
|
604
|
+
const dir = normalized.endsWith("/package.json")
|
|
605
|
+
? normalized.slice(0, -"/package.json".length)
|
|
606
|
+
: normalized;
|
|
607
|
+
return canonicalManifestScopePathsInDir(dir, searchScope, fs);
|
|
585
608
|
}
|
|
586
609
|
if (!normalized.endsWith("/*") || normalized.slice(0, -2).includes("*")) {
|
|
587
610
|
return [];
|
|
@@ -589,9 +612,10 @@ function expandWorkspacePattern(pattern, searchScope, fs) {
|
|
|
589
612
|
const base = normalized.slice(0, -2);
|
|
590
613
|
return safeReadDir(searchScope, fs, base)
|
|
591
614
|
.filter((entry) => entry.isDirectory && !entry.isSymbolicLink)
|
|
592
|
-
.map((entry) =>
|
|
593
|
-
.
|
|
594
|
-
.
|
|
615
|
+
.map((entry) => entry.name)
|
|
616
|
+
.sort()
|
|
617
|
+
.slice(0, MAX_MONOREPO_SERVICE_DIRS)
|
|
618
|
+
.flatMap((name) => canonicalManifestScopePathsInDir(joinScopePath(base, name), searchScope, fs));
|
|
595
619
|
}
|
|
596
620
|
function workspacePackageManifestPaths(input, searchScope, fs) {
|
|
597
621
|
if (input.scope.kind !== "workspace-root" || input.scope.relativePaths.length !== 0) {
|
|
@@ -881,6 +905,37 @@ function selectedFileScopeAtoms(input, searchScope, fs, nowMs) {
|
|
|
881
905
|
}
|
|
882
906
|
return atoms;
|
|
883
907
|
}
|
|
908
|
+
// Accept a candidate injection path once: not already seen, shape-valid, and NOT deny-listed.
|
|
909
|
+
// isDenied is re-checked here (not only at the downstream read gate) so a registry manifest pattern
|
|
910
|
+
// can never inject a deny-listed/secret path as a score-1 atom; registry patterns are also asserted
|
|
911
|
+
// deny-clean in ecosystems.test.ts. Mutates `seen` on acceptance.
|
|
912
|
+
function acceptInjectionScopePath(scopePath, seen) {
|
|
913
|
+
if (seen.has(scopePath) ||
|
|
914
|
+
!isValidScopePath(scopePath, { mustBeRelative: true }) ||
|
|
915
|
+
isDenied(scopePath)) {
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
seen.add(scopePath);
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
// Bound on glob-manifest atoms injected per metadata root from a single directory listing (M4,
|
|
922
|
+
// risk #1). The exact-name loop above handles fixed basenames; this catches GLOB manifests at the
|
|
923
|
+
// root/scope dir (e.g. *.csproj, *.tf) that have no fixed name. Deny-checked + deduped + capped.
|
|
924
|
+
const MAX_ROOT_GLOB_MANIFESTS = 16;
|
|
925
|
+
// Bounded glob-manifest sweep of a single directory: returns the accepted (deduped, deny-clean,
|
|
926
|
+
// shape-valid) scope paths, capped at MAX_ROOT_GLOB_MANIFESTS. Mutates `seen` via the gate.
|
|
927
|
+
function rootGlobManifestPaths(root, searchScope, fs, seen) {
|
|
928
|
+
const paths = [];
|
|
929
|
+
for (const scopePath of canonicalManifestScopePathsInDir(root, searchScope, fs)) {
|
|
930
|
+
if (paths.length >= MAX_ROOT_GLOB_MANIFESTS) {
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
if (acceptInjectionScopePath(scopePath, seen)) {
|
|
934
|
+
paths.push(scopePath);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
return paths;
|
|
938
|
+
}
|
|
884
939
|
function projectMetadataAtoms(input, searchScope, fs, nowMs) {
|
|
885
940
|
if (!wantsProjectMetadata(input)) {
|
|
886
941
|
return [];
|
|
@@ -891,21 +946,21 @@ function projectMetadataAtoms(input, searchScope, fs, nowMs) {
|
|
|
891
946
|
for (const root of metadataRootsForScope(input.scope)) {
|
|
892
947
|
for (const filename of PROJECT_METADATA_FILENAMES) {
|
|
893
948
|
const scopePath = joinScopePath(root, filename);
|
|
894
|
-
if (
|
|
895
|
-
|
|
896
|
-
}
|
|
897
|
-
seen.add(scopePath);
|
|
898
|
-
if (fileExistsInSearchScope(searchScope, fs, scopePath)) {
|
|
949
|
+
if (acceptInjectionScopePath(scopePath, seen) &&
|
|
950
|
+
fileExistsInSearchScope(searchScope, fs, scopePath)) {
|
|
899
951
|
atoms.push(metadataAtom(input.scope, scopePath, queryFingerprint, nowMs));
|
|
900
952
|
}
|
|
901
953
|
}
|
|
954
|
+
// Glob-manifest sweep of the directory itself (bounded), so a root-level *.csproj / *.tf that
|
|
955
|
+
// the fixed-name list cannot enumerate is still injected. Exact names already seen are deduped.
|
|
956
|
+
for (const scopePath of rootGlobManifestPaths(root, searchScope, fs, seen)) {
|
|
957
|
+
atoms.push(metadataAtom(input.scope, scopePath, queryFingerprint, nowMs));
|
|
958
|
+
}
|
|
902
959
|
}
|
|
903
960
|
for (const scopePath of workspacePackageManifestPaths(input, searchScope, fs)) {
|
|
904
|
-
if (
|
|
905
|
-
|
|
961
|
+
if (acceptInjectionScopePath(scopePath, seen)) {
|
|
962
|
+
atoms.push(metadataAtom(input.scope, scopePath, queryFingerprint, nowMs));
|
|
906
963
|
}
|
|
907
|
-
seen.add(scopePath);
|
|
908
|
-
atoms.push(metadataAtom(input.scope, scopePath, queryFingerprint, nowMs));
|
|
909
964
|
}
|
|
910
965
|
return atoms;
|
|
911
966
|
}
|
|
@@ -919,11 +974,8 @@ function repositoryOverviewAtoms(input, searchScope, fs, nowMs) {
|
|
|
919
974
|
for (const root of metadataRootsForScope(input.scope)) {
|
|
920
975
|
for (const filename of REPOSITORY_OVERVIEW_FILENAMES) {
|
|
921
976
|
const scopePath = joinScopePath(root, filename);
|
|
922
|
-
if (
|
|
923
|
-
|
|
924
|
-
}
|
|
925
|
-
seen.add(scopePath);
|
|
926
|
-
if (fileExistsInSearchScope(searchScope, fs, scopePath)) {
|
|
977
|
+
if (acceptInjectionScopePath(scopePath, seen) &&
|
|
978
|
+
fileExistsInSearchScope(searchScope, fs, scopePath)) {
|
|
927
979
|
atoms.push(overviewAtom(input.scope, scopePath, queryFingerprint, nowMs));
|
|
928
980
|
}
|
|
929
981
|
}
|
|
@@ -1248,13 +1300,17 @@ function cachedGroundedPack({ input, deps, plan, rings, ordered, cacheIdentity,
|
|
|
1248
1300
|
excerpts: new Map(),
|
|
1249
1301
|
cacheIdentity,
|
|
1250
1302
|
initialUsage,
|
|
1303
|
+
diagnostics: rings.diagnostics,
|
|
1251
1304
|
}, assembleOptions);
|
|
1252
1305
|
return deps.microIndex.get(key);
|
|
1253
1306
|
}
|
|
1254
1307
|
function preparePackAssembly(input, plan, rings, nowMs) {
|
|
1255
1308
|
const atoms = rings.atoms;
|
|
1256
1309
|
const initialUsage = clampUsageToBudget(rings.governor.usage, plan.budget);
|
|
1257
|
-
|
|
1310
|
+
// M4: pass the classified retrieval intent so ranking can apply intent-conditioned signals
|
|
1311
|
+
// (canonical-metadata, structural-edge). Non-boosted intents (e.g. clarification) and the
|
|
1312
|
+
// no-context default are byte-identical — see weightsForIntent / isIntentBoosted.
|
|
1313
|
+
const ranking = rankCandidates({ atoms, anchors: plan.anchors, context: { retrievalIntent: plan.retrievalIntent } }, { nowMs });
|
|
1258
1314
|
const ordered = refineCandidateOrdering(ranking.kept, ranking.omitted, input.query.text, plan.anchors, nowMs());
|
|
1259
1315
|
return {
|
|
1260
1316
|
atoms,
|
|
@@ -1290,6 +1346,7 @@ async function assemblePackFromReads({ input, plan, rings, prepared, excerptRead
|
|
|
1290
1346
|
? undefined
|
|
1291
1347
|
: cacheIdentity,
|
|
1292
1348
|
initialUsage: prepared.initialUsage,
|
|
1349
|
+
diagnostics: rings.diagnostics,
|
|
1293
1350
|
initialUncertainty: [
|
|
1294
1351
|
...rings.uncertainty,
|
|
1295
1352
|
...excerptReads.uncertainty,
|
|
@@ -1339,13 +1396,24 @@ async function prepareGroundedAssembly(args, augmentedRings, prepared) {
|
|
|
1339
1396
|
});
|
|
1340
1397
|
return { documentEvidence, cached, cacheIdentity, assembleOptions };
|
|
1341
1398
|
}
|
|
1399
|
+
// PR4-W1 (ADR-0055 D1): conditional diagnostics observer. When a ContextProfile is threaded
|
|
1400
|
+
// through OrchestratorDeps, the fully assembled pack is enriched with an additive
|
|
1401
|
+
// `diagnostics.contextBudget?`. The observer is pure and touches no field a prompt builder reads,
|
|
1402
|
+
// so the wire output stays byte-identical (AC5). When the profile is absent, the pack is returned
|
|
1403
|
+
// exactly as assembled — the unchanged-guarantee for legacy callers and existing tests.
|
|
1404
|
+
function withGroundedContextDiagnostics(pack, deps) {
|
|
1405
|
+
if (deps.contextProfile === undefined) {
|
|
1406
|
+
return pack;
|
|
1407
|
+
}
|
|
1408
|
+
return attachContextBudgetDiagnostics(pack, deps.contextProfile);
|
|
1409
|
+
}
|
|
1342
1410
|
async function assembleGroundedPack(args) {
|
|
1343
1411
|
const { input, deps, plan, searchScope, fs, nowMs } = args;
|
|
1344
1412
|
const augmentedRings = await augmentRingsWithDeterministicAtoms(args);
|
|
1345
1413
|
const prepared = preparePackAssembly(input, plan, augmentedRings, nowMs);
|
|
1346
1414
|
const ctx = await prepareGroundedAssembly(args, augmentedRings, prepared);
|
|
1347
1415
|
if (ctx.cached !== undefined) {
|
|
1348
|
-
return ctx.cached;
|
|
1416
|
+
return withGroundedContextDiagnostics(ctx.cached, deps);
|
|
1349
1417
|
}
|
|
1350
1418
|
const excerptReads = await readKeptExcerpts(prepared.keptPaths, {
|
|
1351
1419
|
searchScope,
|
|
@@ -1356,7 +1424,7 @@ async function assembleGroundedPack(args) {
|
|
|
1356
1424
|
nowMs,
|
|
1357
1425
|
signal: deps.signal,
|
|
1358
1426
|
});
|
|
1359
|
-
|
|
1427
|
+
const pack = await assemblePackFromReads({
|
|
1360
1428
|
input,
|
|
1361
1429
|
plan,
|
|
1362
1430
|
rings: augmentedRings,
|
|
@@ -1366,6 +1434,7 @@ async function assembleGroundedPack(args) {
|
|
|
1366
1434
|
cacheIdentity: ctx.cacheIdentity,
|
|
1367
1435
|
assembleOptions: ctx.assembleOptions,
|
|
1368
1436
|
});
|
|
1437
|
+
return withGroundedContextDiagnostics(pack, deps);
|
|
1369
1438
|
}
|
|
1370
1439
|
// ─── Public entry ─────────────────────────────────────────────────────────────
|
|
1371
1440
|
// Epic #532 — retrieval-only pipeline: the ready-governed plan, workspace detection, ring run,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grounded-qa-hybrid.d.ts","sourceRoot":"","sources":["../src/grounded-qa-hybrid.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAE7D,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,qCAAqC,CAAC;AAiB7C,OAAO,EAGL,KAAK,uBAAuB,EAO7B,MAAM,wCAAwC,CAAC;AAIhD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,KAAK,EAAY,aAAa,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAM7C,OAAO,EAML,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAQL,KAAK,2BAA2B,EACjC,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EAEL,KAAK,qBAAqB,EAE3B,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"grounded-qa-hybrid.d.ts","sourceRoot":"","sources":["../src/grounded-qa-hybrid.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAE7D,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,qCAAqC,CAAC;AAiB7C,OAAO,EAGL,KAAK,uBAAuB,EAO7B,MAAM,wCAAwC,CAAC;AAIhD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,KAAK,EAAY,aAAa,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAM7C,OAAO,EAML,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAQL,KAAK,2BAA2B,EACjC,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EAEL,KAAK,qBAAqB,EAE3B,MAAM,sBAAsB,CAAC;AAwB9B,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,uBAAuB,EAAE,CAExF;AAID,wBAAgB,eAAe,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM,EAAE,CAU/E;AAID,MAAM,MAAM,eAAe,GAAG,iBAAiB,CAAC;AAChD,MAAM,MAAM,iBAAiB,GAAG,CAC9B,KAAK,EAAE,cAAc,EACrB,KAAK,EAAE,uBAAuB,EAC9B,QAAQ,EAAE,2BAA2B,KAClC,OAAO,CAAC,eAAe,CAAC,CAAC;AAC9B,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;AAE9F,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC;IAC3C,QAAQ,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IAC/C,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC;IAGjC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS;QACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;KAC1B,EAAE,CAAC;CACL;AAwND,qBAAa,qBAAsB,SAAQ,KAAK;aACX,MAAM,EAAE,WAAW;gBAAnB,MAAM,EAAE,WAAW;CAIvD;AA2FD,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,WAAW,GAClB,cAAc,CAwBhB;AA6eD,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,oBAAoB,GAAG,OAAO,CAAC,WAAW,CAAC,CAS1F"}
|
|
@@ -21,7 +21,7 @@ import { MAX_PROMPT_REFERENCES, buildSelectedScopeSourceLookup, createEmbeddingA
|
|
|
21
21
|
import { GROUNDED_SYSTEM_PROMPT } from "./grounded-prompt.js";
|
|
22
22
|
import { normalizeGroundedAnswerPayload, } from "./grounded-answer.js";
|
|
23
23
|
import { assertUsableAssistantContent } from "./assistant-response.js";
|
|
24
|
-
import { buildCitations, buildQuery, buildSelectedScopeFrom, clarificationRequest, deriveScopeIdFrom, ensureNotCancelled, internalError, isValidGroundedPack, mappedGatewayError, mappedWorkspaceError, persistGroundedExchange, promptSafeExcerptText, redactString, } from "./grounded-qa.js";
|
|
24
|
+
import { buildCitations, buildQuery, buildSelectedScopeFrom, clarificationRequest, deriveScopeIdFrom, ensureNotCancelled, groundedContextAssemblyInput, groundedContextSummaryInput, internalError, isValidGroundedPack, mappedGatewayError, mappedWorkspaceError, persistGroundedExchange, promptSafeExcerptText, redactString, } from "./grounded-qa.js";
|
|
25
25
|
// ─── Canonical connector reader ───────────────────────────────────────────────
|
|
26
26
|
// Mirrors buildConnectedScopes: the plural `localKnowledgeScopes` list supersedes the legacy single
|
|
27
27
|
// `localKnowledgeScope`. Readers must not mix the two — the list, when present, is authoritative.
|
|
@@ -326,10 +326,10 @@ function emptyFolderSummary() {
|
|
|
326
326
|
elapsedMs: 0,
|
|
327
327
|
};
|
|
328
328
|
}
|
|
329
|
-
function folderSummary(folders, redactor) {
|
|
329
|
+
function folderSummary(folders, redactor, deps) {
|
|
330
330
|
if (folders.length === 0)
|
|
331
331
|
return emptyFolderSummary();
|
|
332
|
-
return mergeContextPackSummaries(folders.map((src) => buildGroundedAnswerContextPackSummary(src.pack, buildCitations(src.pack, redactor).length, src.elapsedMs)));
|
|
332
|
+
return mergeContextPackSummaries(folders.map((src) => buildGroundedAnswerContextPackSummary(src.pack, buildCitations(src.pack, redactor).length, src.elapsedMs, groundedContextSummaryInput(deps, src.pack))));
|
|
333
333
|
}
|
|
334
334
|
function hashString32(value) {
|
|
335
335
|
let hash = 0x811c9dc5;
|
|
@@ -404,6 +404,7 @@ function persistFolderEvidence(ctx, folders) {
|
|
|
404
404
|
elapsedMs: src.elapsedMs,
|
|
405
405
|
startedAt,
|
|
406
406
|
finishedAt,
|
|
407
|
+
...groundedContextAssemblyInput(ctx.deps, src.pack),
|
|
407
408
|
}, {
|
|
408
409
|
store: ctx.deps.evidenceStore,
|
|
409
410
|
env: ctx.deps.env,
|
|
@@ -535,7 +536,7 @@ function assembleHybridAnswer(ctx, sources, store, selected, limits, assistant,
|
|
|
535
536
|
const { firstRunId: evidenceRunId, runIds: evidenceRunIds } = persistFolderEvidence(ctx, sources.folders);
|
|
536
537
|
persistConnectorAudit(store, sources.connectors, selected, ctx.modelId);
|
|
537
538
|
const elapsedMs = sources.folders.reduce((acc, src) => acc + src.elapsedMs, 0);
|
|
538
|
-
const summary = folderSummary(sources.folders, redactor);
|
|
539
|
+
const summary = folderSummary(sources.folders, redactor, ctx.deps);
|
|
539
540
|
return {
|
|
540
541
|
groundingKind: "hybrid",
|
|
541
542
|
...ids,
|
|
@@ -575,6 +576,7 @@ function assembleHybridNoEvidenceRoute(ctx, store, meta, selected, limits) {
|
|
|
575
576
|
folderSourceCount: meta.folderScopeCount,
|
|
576
577
|
connectorSourceCount: meta.connectorScopeCount,
|
|
577
578
|
}, store, selected, limits, { content, usage: { promptTokens: 0, completionTokens: 0 } }, { userMessageId: userMessage.id, assistantMessageId: assistantMessage.id });
|
|
579
|
+
ctx.deps.store.attachGroundedAnswer(assistantMessage.id, answer);
|
|
578
580
|
return { status: 200, body: answer };
|
|
579
581
|
}
|
|
580
582
|
export async function runHybridGroundedAsk(ctx) {
|
|
@@ -736,6 +738,7 @@ async function answerAndAssemble(ctx, store, meta) {
|
|
|
736
738
|
folderSourceCount: meta.folderScopeCount,
|
|
737
739
|
connectorSourceCount: meta.connectorScopeCount,
|
|
738
740
|
}, store, selected, limits, assistant, { userMessageId: userMessage.id, assistantMessageId: assistantMessage.id });
|
|
741
|
+
ctx.deps.store.attachGroundedAnswer(assistantMessage.id, answer);
|
|
739
742
|
return { status: 200, body: answer };
|
|
740
743
|
}
|
|
741
744
|
// Issue #154 (GAP-B) — a GatewayError is redacted inside mappedGatewayError (shared with the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grounded-qa-multi-source.d.ts","sourceRoot":"","sources":["../src/grounded-qa-multi-source.ts"],"names":[],"mappings":"AAWA,OAAO,EAGL,KAAK,WAAW,IAAI,kBAAkB,EACvC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"grounded-qa-multi-source.d.ts","sourceRoot":"","sources":["../src/grounded-qa-multi-source.ts"],"names":[],"mappings":"AAWA,OAAO,EAGL,KAAK,WAAW,IAAI,kBAAkB,EACvC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAQ7D,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EAGvB,MAAM,iDAAiD,CAAC;AACzD,OAAO,EAEL,KAAK,kBAAkB,EAGvB,KAAK,gCAAgC,EAGtC,MAAM,wCAAwC,CAAC;AAEhD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAIL,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACzB,MAAM,4BAA4B,CAAC;AAKpC,OAAO,EAEL,KAAK,qBAAqB,EAE3B,MAAM,sBAAsB,CAAC;AA4B9B,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,kBAAkB,EAAE,CAE9E;AAOD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,iBAAiB,EAAE,CAAC,EAAE,MAAM,GAAG,iBAAiB,CAY5F;AAgBD,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,kBAAkB,EAAE,GAAG,SAAS,MAAM,EAAE,CAUrF;AAoID,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,SAAS,gCAAgC,EAAE,GACrD,gCAAgC,CA0BlC;AAID,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;CACrC;AAoBD,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,SAAS,WAAW,EAAE,EACpC,QAAQ,EAAE,QAAQ,GACjB,SAAS,kBAAkB,EAAE,CAE/B;AAmFD,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAI3F,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,iBAAiB,CAUvE;AAID,MAAM,MAAM,mBAAmB,GAAG,CAChC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,SAAS,WAAW,EAAE,KACjC,OAAO,CAAC,qBAAqB,CAAC,CAAC;AAEpC,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,SAAS,EAChB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,WAAW,GAClB,mBAAmB,CAqBrB;AAyBD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAC/C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,iBAAiB,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAG7B,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACvF;AAkND,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAAC,WAAW,CAAC,CA6CtF"}
|
|
@@ -10,6 +10,7 @@ import { basename } from "node:path";
|
|
|
10
10
|
import { createHash, randomUUID } from "node:crypto";
|
|
11
11
|
import { ContextOverflowError, resolveCostClass, } from "@oscharko-dev/keiko-model-gateway";
|
|
12
12
|
import { persistConnectedContextEvidence } from "@oscharko-dev/keiko-evidence";
|
|
13
|
+
import { CONTEXT_LANE_IDS, } from "@oscharko-dev/keiko-contracts";
|
|
13
14
|
import { CANDIDATE_OMISSION_REASONS, DEFAULT_EXPLORATION_BUDGET, } from "@oscharko-dev/keiko-contracts/connected-context";
|
|
14
15
|
import { buildGroundedAnswerContextPackSummary, } from "@oscharko-dev/keiko-contracts/bff-wire";
|
|
15
16
|
import { currentRedactionSecrets } from "./deps.js";
|
|
@@ -19,7 +20,7 @@ import { GROUNDED_SYSTEM_PROMPT } from "./grounded-prompt.js";
|
|
|
19
20
|
import { rememberGroundedTurn } from "./grounded-turn-registry.js";
|
|
20
21
|
import { assertUsableAssistantContent } from "./assistant-response.js";
|
|
21
22
|
import { normalizeGroundedAnswerPayload, } from "./grounded-answer.js";
|
|
22
|
-
import { buildCitations, buildQuery, buildSelectedScopeFrom, clarificationRequest, deriveScopeIdFrom, ensureNotCancelled, evidenceLines, internalError, isValidGroundedPack, mappedGatewayError, mappedWorkspaceError, modelInputPromptByteLimit, packBudgetSummary, persistGroundedExchange, promptByteLength, redactString, uncertaintyLines, withPromptExcerptByteLimit, } from "./grounded-qa.js";
|
|
23
|
+
import { buildCitations, buildQuery, buildSelectedScopeFrom, clarificationRequest, deriveScopeIdFrom, ensureNotCancelled, evidenceLines, groundedContextAssemblyInput, groundedContextSummaryInput, internalError, isValidGroundedPack, mappedGatewayError, mappedWorkspaceError, modelInputPromptByteLimit, packBudgetSummary, persistGroundedExchange, promptByteLength, redactString, uncertaintyLines, withPromptExcerptByteLimit, } from "./grounded-qa.js";
|
|
23
24
|
// ─── Canonical reader + label/budget helpers ──────────────────────────────────
|
|
24
25
|
// Canonical reader rule (Epic #532 contract): `connectedScopes` supersedes the legacy single
|
|
25
26
|
// `connectedScope`. Readers must NOT mix the two — the list, when present, is authoritative.
|
|
@@ -130,11 +131,54 @@ function mergedFileCount(summaries) {
|
|
|
130
131
|
return -1;
|
|
131
132
|
return summaries.reduce((acc, s) => acc + s.fileCount, 0);
|
|
132
133
|
}
|
|
134
|
+
const PRESSURE_RANK = {
|
|
135
|
+
low: 0,
|
|
136
|
+
moderate: 1,
|
|
137
|
+
high: 2,
|
|
138
|
+
exceeded: 3,
|
|
139
|
+
};
|
|
140
|
+
function isContextSummary(summary) {
|
|
141
|
+
return summary !== undefined;
|
|
142
|
+
}
|
|
143
|
+
function emptyLaneCounts() {
|
|
144
|
+
const counts = {};
|
|
145
|
+
for (const laneId of CONTEXT_LANE_IDS) {
|
|
146
|
+
counts[laneId] = 0;
|
|
147
|
+
}
|
|
148
|
+
return counts;
|
|
149
|
+
}
|
|
150
|
+
function worstPressure(current, next) {
|
|
151
|
+
return PRESSURE_RANK[next] > PRESSURE_RANK[current] ? next : current;
|
|
152
|
+
}
|
|
153
|
+
function mergeContextSummaries(summaries) {
|
|
154
|
+
const contextSummaries = summaries
|
|
155
|
+
.map((summary) => summary.contextSummary)
|
|
156
|
+
.filter(isContextSummary);
|
|
157
|
+
if (contextSummaries.length === 0) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
const laneCounts = emptyLaneCounts();
|
|
161
|
+
let totalEstimatedTokens = 0;
|
|
162
|
+
let budgetPressure = "low";
|
|
163
|
+
let compactionActive = false;
|
|
164
|
+
for (const summary of contextSummaries) {
|
|
165
|
+
totalEstimatedTokens += summary.totalEstimatedTokens;
|
|
166
|
+
budgetPressure = worstPressure(budgetPressure, summary.budgetPressure);
|
|
167
|
+
compactionActive ||= summary.compactionActive;
|
|
168
|
+
for (const laneId of CONTEXT_LANE_IDS) {
|
|
169
|
+
laneCounts[laneId] += summary.laneCounts[laneId];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { totalEstimatedTokens, budgetPressure, laneCounts, compactionActive };
|
|
173
|
+
}
|
|
133
174
|
export function mergeContextPackSummaries(summaries) {
|
|
134
175
|
const [first] = summaries;
|
|
135
176
|
if (first === undefined) {
|
|
136
177
|
throw new Error("mergeContextPackSummaries requires at least one summary");
|
|
137
178
|
}
|
|
179
|
+
// ADR-0057 D1: merge every contributing source's path-free contextSummary. The projection remains
|
|
180
|
+
// structurally path-free: fixed lane-id keys, numeric counts/tokens, a pressure enum, and a boolean.
|
|
181
|
+
const mergedContextSummary = mergeContextSummaries(summaries);
|
|
138
182
|
return {
|
|
139
183
|
schemaVersion: first.schemaVersion,
|
|
140
184
|
scopeId: `scope-${createHash("sha256")
|
|
@@ -151,6 +195,7 @@ export function mergeContextPackSummaries(summaries) {
|
|
|
151
195
|
omittedCounts: mergeOmittedCounts(summaries),
|
|
152
196
|
uncertaintyCount: summaries.reduce((acc, s) => acc + s.uncertaintyCount, 0),
|
|
153
197
|
elapsedMs: summaries.reduce((acc, s) => acc + s.elapsedMs, 0),
|
|
198
|
+
...(mergedContextSummary !== undefined ? { contextSummary: mergedContextSummary } : {}),
|
|
154
199
|
};
|
|
155
200
|
}
|
|
156
201
|
function sourceSection(entry, index, redactor) {
|
|
@@ -369,6 +414,7 @@ function persistPerSourceEvidence(ctx, sources) {
|
|
|
369
414
|
elapsedMs: src.elapsedMs,
|
|
370
415
|
startedAt,
|
|
371
416
|
finishedAt,
|
|
417
|
+
...groundedContextAssemblyInput(ctx.deps, src.pack),
|
|
372
418
|
}, {
|
|
373
419
|
store: ctx.deps.evidenceStore,
|
|
374
420
|
env: ctx.deps.env,
|
|
@@ -383,7 +429,7 @@ function persistPerSourceEvidence(ctx, sources) {
|
|
|
383
429
|
function assembleMultiSourceAnswer(ctx, sources, skipped, assistant, ids) {
|
|
384
430
|
const { redactor } = ctx.deps;
|
|
385
431
|
const citations = mergedCitations(sources, redactor);
|
|
386
|
-
const summaries = sources.map((src) => buildGroundedAnswerContextPackSummary(src.pack, buildCitations(src.pack, redactor).length, src.elapsedMs));
|
|
432
|
+
const summaries = sources.map((src) => buildGroundedAnswerContextPackSummary(src.pack, buildCitations(src.pack, redactor).length, src.elapsedMs, groundedContextSummaryInput(ctx.deps, src.pack)));
|
|
387
433
|
const mergedSummary = mergeContextPackSummaries(summaries);
|
|
388
434
|
const { firstRunId, runIds } = persistPerSourceEvidence(ctx, sources);
|
|
389
435
|
return {
|
|
@@ -435,6 +481,7 @@ export async function runMultiSourceAsk(ctx) {
|
|
|
435
481
|
userMessageId: userMessage.id,
|
|
436
482
|
assistantMessageId: assistantMessage.id,
|
|
437
483
|
});
|
|
484
|
+
ctx.deps.store.attachGroundedAnswer(assistantMessage.id, answer);
|
|
438
485
|
rememberGroundedTurn({
|
|
439
486
|
assistantMessageId: assistantMessage.id,
|
|
440
487
|
chatId: ctx.chat.id,
|
package/dist/grounded-qa.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type ChatMessage as GatewayChatMessage } from "@oscharko-dev/keiko-model-gateway";
|
|
2
|
+
import { type ConnectedContextEvidenceInput } from "@oscharko-dev/keiko-evidence";
|
|
2
3
|
import { type ConnectedContextPack, type RetrievalQuery, type SelectedScope } from "@oscharko-dev/keiko-contracts/connected-context";
|
|
3
4
|
import { type GroundedEvidenceCitation, type GroundedUncertainty } from "@oscharko-dev/keiko-contracts/bff-wire";
|
|
4
5
|
import type { RouteContext, RouteResult } from "./routes.js";
|
|
5
6
|
import type { Redactor, UiHandlerDeps } from "./deps.js";
|
|
6
7
|
import type { Chat, ChatConnectedScope, ChatMessage } from "./store/index.js";
|
|
7
8
|
import { type OrchestratorInput, type OrchestratorOutput } from "./grounded-orchestrator.js";
|
|
9
|
+
import { deriveGroundedContextAssembly } from "./grounded-context-diagnostics.js";
|
|
8
10
|
import { type GroundedRetriever, type MultiSourceAnswerer } from "./grounded-qa-multi-source.js";
|
|
9
11
|
import { type ConnectorRetrieve, type FolderRetriever, type HybridAnswerer } from "./grounded-qa-hybrid.js";
|
|
10
12
|
import { GROUNDED_SYSTEM_PROMPT } from "./grounded-prompt.js";
|
|
@@ -32,6 +34,8 @@ export declare function buildCitations(pack: ConnectedContextPack, redactor: Red
|
|
|
32
34
|
export declare function buildUncertainty(pack: ConnectedContextPack, redactor: Redactor): readonly GroundedUncertainty[];
|
|
33
35
|
export type GroundedRunner = (input: OrchestratorInput) => Promise<OrchestratorOutput>;
|
|
34
36
|
export declare function persistGroundedExchange(deps: UiHandlerDeps, chatId: string, userContent: string, assistantContent: string): readonly [ChatMessage, ChatMessage];
|
|
37
|
+
export declare function groundedContextAssemblyInput(deps: Pick<UiHandlerDeps, "contextProfile">, pack: ConnectedContextPack): Pick<ConnectedContextEvidenceInput, "contextAssembly">;
|
|
38
|
+
export declare function groundedContextSummaryInput(deps: Pick<UiHandlerDeps, "contextProfile">, pack: ConnectedContextPack): ReturnType<typeof deriveGroundedContextAssembly> | undefined;
|
|
35
39
|
export interface MultiSourceSeam {
|
|
36
40
|
readonly retriever: GroundedRetriever;
|
|
37
41
|
readonly answerer: MultiSourceAnswerer;
|