@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.
Files changed (60) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/chat-compaction-evidence.d.ts +12 -0
  3. package/dist/chat-compaction-evidence.d.ts.map +1 -0
  4. package/dist/chat-compaction-evidence.js +46 -0
  5. package/dist/chat-handlers.d.ts +16 -0
  6. package/dist/chat-handlers.d.ts.map +1 -1
  7. package/dist/chat-handlers.js +78 -28
  8. package/dist/chat-stream-handlers.d.ts.map +1 -1
  9. package/dist/chat-stream-handlers.js +13 -1
  10. package/dist/conversation-compaction.d.ts +12 -0
  11. package/dist/conversation-compaction.d.ts.map +1 -0
  12. package/dist/conversation-compaction.js +102 -0
  13. package/dist/deps.d.ts +3 -0
  14. package/dist/deps.d.ts.map +1 -1
  15. package/dist/deps.js +3 -2
  16. package/dist/files.d.ts +18 -0
  17. package/dist/files.d.ts.map +1 -1
  18. package/dist/files.js +174 -0
  19. package/dist/gateway-readiness.d.ts +6 -0
  20. package/dist/gateway-readiness.d.ts.map +1 -0
  21. package/dist/gateway-readiness.js +624 -0
  22. package/dist/grounded-context-diagnostics.d.ts +5 -0
  23. package/dist/grounded-context-diagnostics.d.ts.map +1 -0
  24. package/dist/grounded-context-diagnostics.js +77 -0
  25. package/dist/grounded-orchestrator.d.ts +2 -0
  26. package/dist/grounded-orchestrator.d.ts.map +1 -1
  27. package/dist/grounded-orchestrator.js +122 -53
  28. package/dist/grounded-qa-hybrid.d.ts.map +1 -1
  29. package/dist/grounded-qa-hybrid.js +7 -4
  30. package/dist/grounded-qa-multi-source.d.ts.map +1 -1
  31. package/dist/grounded-qa-multi-source.js +49 -2
  32. package/dist/grounded-qa.d.ts +4 -0
  33. package/dist/grounded-qa.d.ts.map +1 -1
  34. package/dist/grounded-qa.js +36 -2
  35. package/dist/index.d.ts +3 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +6 -1
  38. package/dist/local-knowledge-grounded-qa.d.ts.map +1 -1
  39. package/dist/local-knowledge-grounded-qa.js +11 -2
  40. package/dist/routes.d.ts.map +1 -1
  41. package/dist/routes.js +5 -10
  42. package/dist/run-handlers.d.ts +0 -1
  43. package/dist/run-handlers.d.ts.map +1 -1
  44. package/dist/run-handlers.js +0 -217
  45. package/dist/store/db.d.ts.map +1 -1
  46. package/dist/store/db.js +2 -1
  47. package/dist/store/index.d.ts +1 -1
  48. package/dist/store/index.d.ts.map +1 -1
  49. package/dist/store/messages.d.ts +2 -1
  50. package/dist/store/messages.d.ts.map +1 -1
  51. package/dist/store/messages.js +46 -4
  52. package/dist/store/schema.d.ts +1 -1
  53. package/dist/store/schema.d.ts.map +1 -1
  54. package/dist/store/schema.js +7 -1
  55. package/dist/store/types.d.ts +3 -2
  56. package/dist/store/types.d.ts.map +1 -1
  57. package/package.json +19 -19
  58. package/dist/grounded-handoff.d.ts +0 -4
  59. package/dist/grounded-handoff.d.ts.map +0 -1
  60. 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,EAEzB,KAAK,iBAAiB,EAGtB,KAAK,cAAc,EACnB,KAAK,aAAa,EAEnB,MAAM,iDAAiD,CAAC;AACzD,OAAO,EAUL,KAAK,mBAAmB,EAExB,KAAK,eAAe,EAEpB,KAAK,UAAU,EAIhB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,qBAAqB,EAerB,KAAK,WAAW,EAChB,KAAK,aAAa,EAGnB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAkC,KAAK,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AASlG,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;CAClC;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;AA25BD,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAM/E;AAw5BD,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"}
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
- const PROJECT_METADATA_FILENAMES = [
370
- "package.json",
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
- "vitest.config.ts",
377
- "vitest.config.mts",
378
- "vitest.config.js",
379
- "vitest.config.mjs",
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 scopePath = normalized.endsWith("/package.json")
582
- ? normalized
583
- : joinScopePath(normalized, "package.json");
584
- return fileExistsInSearchScope(searchScope, fs, scopePath) ? [scopePath] : [];
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) => joinScopePath(joinScopePath(base, entry.name), "package.json"))
593
- .filter((scopePath) => fileExistsInSearchScope(searchScope, fs, scopePath))
594
- .sort();
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 (seen.has(scopePath) || !isValidScopePath(scopePath, { mustBeRelative: true })) {
895
- continue;
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 (seen.has(scopePath) || !isValidScopePath(scopePath, { mustBeRelative: true })) {
905
- continue;
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 (seen.has(scopePath) || !isValidScopePath(scopePath, { mustBeRelative: true })) {
923
- continue;
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
- const ranking = rankCandidates({ atoms, anchors: plan.anchors }, { nowMs });
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
- return await assemblePackFromReads({
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;AAsB9B,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;AA4FD,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,WAAW,GAClB,cAAc,CAwBhB;AA4eD,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,oBAAoB,GAAG,OAAO,CAAC,WAAW,CAAC,CAS1F"}
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;AAG7D,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EAGvB,MAAM,iDAAiD,CAAC;AACzD,OAAO,EAEL,KAAK,kBAAkB,EAEvB,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;AA0B9B,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;AAgFD,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,SAAS,gCAAgC,EAAE,GACrD,gCAAgC,CAsBlC;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;AAgND,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAAC,WAAW,CAAC,CA4CtF"}
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,
@@ -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;