@oscharko-dev/keiko-workflows 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/bug-investigation/context.d.ts +7 -0
  3. package/dist/bug-investigation/context.d.ts.map +1 -0
  4. package/dist/bug-investigation/context.js +119 -0
  5. package/dist/bug-investigation/descriptor.d.ts +4 -0
  6. package/dist/bug-investigation/descriptor.d.ts.map +1 -0
  7. package/dist/bug-investigation/descriptor.js +46 -0
  8. package/dist/bug-investigation/emit.d.ts +13 -0
  9. package/dist/bug-investigation/emit.d.ts.map +1 -0
  10. package/dist/bug-investigation/emit.js +35 -0
  11. package/dist/bug-investigation/events.d.ts +2 -0
  12. package/dist/bug-investigation/events.d.ts.map +1 -0
  13. package/dist/bug-investigation/events.js +6 -0
  14. package/dist/bug-investigation/failure-parse.d.ts +4 -0
  15. package/dist/bug-investigation/failure-parse.d.ts.map +1 -0
  16. package/dist/bug-investigation/failure-parse.js +154 -0
  17. package/dist/bug-investigation/guard.d.ts +3 -0
  18. package/dist/bug-investigation/guard.d.ts.map +1 -0
  19. package/dist/bug-investigation/guard.js +69 -0
  20. package/dist/bug-investigation/index.d.ts +8 -0
  21. package/dist/bug-investigation/index.d.ts.map +1 -0
  22. package/dist/bug-investigation/index.js +13 -0
  23. package/dist/bug-investigation/internal.d.ts +39 -0
  24. package/dist/bug-investigation/internal.d.ts.map +1 -0
  25. package/dist/bug-investigation/internal.js +65 -0
  26. package/dist/bug-investigation/memory.d.ts +5 -0
  27. package/dist/bug-investigation/memory.d.ts.map +1 -0
  28. package/dist/bug-investigation/memory.js +91 -0
  29. package/dist/bug-investigation/model-loop.d.ts +5 -0
  30. package/dist/bug-investigation/model-loop.d.ts.map +1 -0
  31. package/dist/bug-investigation/model-loop.js +225 -0
  32. package/dist/bug-investigation/parse.d.ts +4 -0
  33. package/dist/bug-investigation/parse.d.ts.map +1 -0
  34. package/dist/bug-investigation/parse.js +125 -0
  35. package/dist/bug-investigation/prompt.d.ts +5 -0
  36. package/dist/bug-investigation/prompt.d.ts.map +1 -0
  37. package/dist/bug-investigation/prompt.js +122 -0
  38. package/dist/bug-investigation/report.d.ts +24 -0
  39. package/dist/bug-investigation/report.d.ts.map +1 -0
  40. package/dist/bug-investigation/report.js +151 -0
  41. package/dist/bug-investigation/stages.d.ts +14 -0
  42. package/dist/bug-investigation/stages.d.ts.map +1 -0
  43. package/dist/bug-investigation/stages.js +247 -0
  44. package/dist/bug-investigation/types.d.ts +88 -0
  45. package/dist/bug-investigation/types.d.ts.map +1 -0
  46. package/dist/bug-investigation/types.js +6 -0
  47. package/dist/bug-investigation/verify-stage.d.ts +11 -0
  48. package/dist/bug-investigation/verify-stage.d.ts.map +1 -0
  49. package/dist/bug-investigation/verify-stage.js +91 -0
  50. package/dist/bug-investigation/workflow.d.ts +3 -0
  51. package/dist/bug-investigation/workflow.d.ts.map +1 -0
  52. package/dist/bug-investigation/workflow.js +85 -0
  53. package/dist/contextpack/assemble.d.ts +35 -0
  54. package/dist/contextpack/assemble.d.ts.map +1 -0
  55. package/dist/contextpack/assemble.js +431 -0
  56. package/dist/contextpack/compaction.d.ts +23 -0
  57. package/dist/contextpack/compaction.d.ts.map +1 -0
  58. package/dist/contextpack/compaction.js +68 -0
  59. package/dist/contextpack/index.d.ts +9 -0
  60. package/dist/contextpack/index.d.ts.map +1 -0
  61. package/dist/contextpack/index.js +8 -0
  62. package/dist/contextpack/microIndex.d.ts +29 -0
  63. package/dist/contextpack/microIndex.d.ts.map +1 -0
  64. package/dist/contextpack/microIndex.js +98 -0
  65. package/dist/contextpack/reranker.d.ts +15 -0
  66. package/dist/contextpack/reranker.d.ts.map +1 -0
  67. package/dist/contextpack/reranker.js +31 -0
  68. package/dist/descriptor.d.ts +2 -0
  69. package/dist/descriptor.d.ts.map +1 -0
  70. package/dist/descriptor.js +1 -0
  71. package/dist/governed-handoff.d.ts +6 -0
  72. package/dist/governed-handoff.d.ts.map +1 -0
  73. package/dist/governed-handoff.js +86 -0
  74. package/dist/index.d.ts +9 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +13 -0
  77. package/dist/planner/anchors.d.ts +17 -0
  78. package/dist/planner/anchors.d.ts.map +1 -0
  79. package/dist/planner/anchors.js +291 -0
  80. package/dist/planner/explorationPlanner.d.ts +9 -0
  81. package/dist/planner/explorationPlanner.d.ts.map +1 -0
  82. package/dist/planner/explorationPlanner.js +15 -0
  83. package/dist/planner/governor.d.ts +16 -0
  84. package/dist/planner/governor.d.ts.map +1 -0
  85. package/dist/planner/governor.js +106 -0
  86. package/dist/planner/index.d.ts +11 -0
  87. package/dist/planner/index.d.ts.map +1 -0
  88. package/dist/planner/index.js +8 -0
  89. package/dist/planner/intent.d.ts +8 -0
  90. package/dist/planner/intent.d.ts.map +1 -0
  91. package/dist/planner/intent.js +140 -0
  92. package/dist/planner/plan.d.ts +43 -0
  93. package/dist/planner/plan.d.ts.map +1 -0
  94. package/dist/planner/plan.js +237 -0
  95. package/dist/promptEnhancer/index.d.ts +23 -0
  96. package/dist/promptEnhancer/index.d.ts.map +1 -0
  97. package/dist/promptEnhancer/index.js +282 -0
  98. package/dist/qualityIntelligence/__tests__/fixtures/runEntryFixtures.d.ts +30 -0
  99. package/dist/qualityIntelligence/__tests__/fixtures/runEntryFixtures.d.ts.map +1 -0
  100. package/dist/qualityIntelligence/__tests__/fixtures/runEntryFixtures.js +114 -0
  101. package/dist/qualityIntelligence/cancellation.d.ts +20 -0
  102. package/dist/qualityIntelligence/cancellation.d.ts.map +1 -0
  103. package/dist/qualityIntelligence/cancellation.js +55 -0
  104. package/dist/qualityIntelligence/descriptors.d.ts +41 -0
  105. package/dist/qualityIntelligence/descriptors.d.ts.map +1 -0
  106. package/dist/qualityIntelligence/descriptors.js +105 -0
  107. package/dist/qualityIntelligence/index.d.ts +11 -0
  108. package/dist/qualityIntelligence/index.d.ts.map +1 -0
  109. package/dist/qualityIntelligence/index.js +11 -0
  110. package/dist/qualityIntelligence/modelRoutedTestDesign.d.ts +100 -0
  111. package/dist/qualityIntelligence/modelRoutedTestDesign.d.ts.map +1 -0
  112. package/dist/qualityIntelligence/modelRoutedTestDesign.js +620 -0
  113. package/dist/qualityIntelligence/runEntries.d.ts +60 -0
  114. package/dist/qualityIntelligence/runEntries.d.ts.map +1 -0
  115. package/dist/qualityIntelligence/runEntries.js +243 -0
  116. package/dist/qualityIntelligence/runtimeCommon.d.ts +106 -0
  117. package/dist/qualityIntelligence/runtimeCommon.d.ts.map +1 -0
  118. package/dist/qualityIntelligence/runtimeCommon.js +258 -0
  119. package/dist/qualityIntelligence/scopedRegeneration.d.ts +26 -0
  120. package/dist/qualityIntelligence/scopedRegeneration.d.ts.map +1 -0
  121. package/dist/qualityIntelligence/scopedRegeneration.js +35 -0
  122. package/dist/ranking/filter.d.ts +20 -0
  123. package/dist/ranking/filter.d.ts.map +1 -0
  124. package/dist/ranking/filter.js +99 -0
  125. package/dist/ranking/index.d.ts +9 -0
  126. package/dist/ranking/index.d.ts.map +1 -0
  127. package/dist/ranking/index.js +8 -0
  128. package/dist/ranking/rank.d.ts +21 -0
  129. package/dist/ranking/rank.d.ts.map +1 -0
  130. package/dist/ranking/rank.js +160 -0
  131. package/dist/ranking/scoring.d.ts +13 -0
  132. package/dist/ranking/scoring.d.ts.map +1 -0
  133. package/dist/ranking/scoring.js +39 -0
  134. package/dist/ranking/signals.d.ts +20 -0
  135. package/dist/ranking/signals.d.ts.map +1 -0
  136. package/dist/ranking/signals.js +145 -0
  137. package/dist/unit-tests/context.d.ts +7 -0
  138. package/dist/unit-tests/context.d.ts.map +1 -0
  139. package/dist/unit-tests/context.js +129 -0
  140. package/dist/unit-tests/conventions.d.ts +5 -0
  141. package/dist/unit-tests/conventions.d.ts.map +1 -0
  142. package/dist/unit-tests/conventions.js +87 -0
  143. package/dist/unit-tests/descriptor.d.ts +5 -0
  144. package/dist/unit-tests/descriptor.d.ts.map +1 -0
  145. package/dist/unit-tests/descriptor.js +43 -0
  146. package/dist/unit-tests/emit.d.ts +13 -0
  147. package/dist/unit-tests/emit.d.ts.map +1 -0
  148. package/dist/unit-tests/emit.js +35 -0
  149. package/dist/unit-tests/events.d.ts +2 -0
  150. package/dist/unit-tests/events.d.ts.map +1 -0
  151. package/dist/unit-tests/events.js +6 -0
  152. package/dist/unit-tests/frontend.d.ts +42 -0
  153. package/dist/unit-tests/frontend.d.ts.map +1 -0
  154. package/dist/unit-tests/frontend.js +281 -0
  155. package/dist/unit-tests/index.d.ts +9 -0
  156. package/dist/unit-tests/index.d.ts.map +1 -0
  157. package/dist/unit-tests/index.js +15 -0
  158. package/dist/unit-tests/internal.d.ts +36 -0
  159. package/dist/unit-tests/internal.d.ts.map +1 -0
  160. package/dist/unit-tests/internal.js +43 -0
  161. package/dist/unit-tests/model-loop.d.ts +6 -0
  162. package/dist/unit-tests/model-loop.d.ts.map +1 -0
  163. package/dist/unit-tests/model-loop.js +98 -0
  164. package/dist/unit-tests/parse.d.ts +7 -0
  165. package/dist/unit-tests/parse.d.ts.map +1 -0
  166. package/dist/unit-tests/parse.js +68 -0
  167. package/dist/unit-tests/prompt.d.ts +6 -0
  168. package/dist/unit-tests/prompt.d.ts.map +1 -0
  169. package/dist/unit-tests/prompt.js +139 -0
  170. package/dist/unit-tests/report.d.ts +26 -0
  171. package/dist/unit-tests/report.d.ts.map +1 -0
  172. package/dist/unit-tests/report.js +104 -0
  173. package/dist/unit-tests/stages.d.ts +12 -0
  174. package/dist/unit-tests/stages.d.ts.map +1 -0
  175. package/dist/unit-tests/stages.js +202 -0
  176. package/dist/unit-tests/strategy.d.ts +6 -0
  177. package/dist/unit-tests/strategy.d.ts.map +1 -0
  178. package/dist/unit-tests/strategy.js +36 -0
  179. package/dist/unit-tests/target-guard.d.ts +5 -0
  180. package/dist/unit-tests/target-guard.d.ts.map +1 -0
  181. package/dist/unit-tests/target-guard.js +29 -0
  182. package/dist/unit-tests/types.d.ts +74 -0
  183. package/dist/unit-tests/types.d.ts.map +1 -0
  184. package/dist/unit-tests/types.js +6 -0
  185. package/dist/unit-tests/verify-stage.d.ts +10 -0
  186. package/dist/unit-tests/verify-stage.d.ts.map +1 -0
  187. package/dist/unit-tests/verify-stage.js +56 -0
  188. package/dist/unit-tests/workflow.d.ts +3 -0
  189. package/dist/unit-tests/workflow.d.ts.map +1 -0
  190. package/dist/unit-tests/workflow.js +69 -0
  191. package/package.json +38 -0
@@ -0,0 +1,431 @@
1
+ // Public facade for the connected-context pack assembler (Epic #177, Issue #183). Bridges
2
+ // ranked candidates (#182) + already-redacted excerpt content into a ConnectedContextPack
3
+ // per the #178 contract. Pure orchestration: compaction + budget checkpointing + reranker
4
+ // seam + optional micro-index. No IO. The audit ledger (#187) owns persistence and
5
+ // `ledgerRef` is therefore always undefined here.
6
+ import { createHash } from "node:crypto";
7
+ import { CONNECTED_CONTEXT_SCHEMA_VERSION, } from "@oscharko-dev/keiko-contracts/connected-context";
8
+ import { connectedContextPackStableId } from "@oscharko-dev/keiko-workspace";
9
+ import { compactExcerpt, nextAtomFitsBudget } from "./compaction.js";
10
+ import { makeIndexKey } from "./microIndex.js";
11
+ import { disabledReranker } from "./reranker.js";
12
+ // ─── Constants ────────────────────────────────────────────────────────────────
13
+ const DEFAULT_MAX_BYTES_PER_EXCERPT = 8 * 1024;
14
+ function resolveOptions(options) {
15
+ return {
16
+ maxBytesPerExcerpt: options?.maxBytesPerExcerpt ?? DEFAULT_MAX_BYTES_PER_EXCERPT,
17
+ editablePaths: options?.editablePaths ?? new Set(),
18
+ reranker: options?.reranker ?? disabledReranker,
19
+ microIndex: options?.microIndex,
20
+ nowMs: options?.nowMs ?? Date.now,
21
+ };
22
+ }
23
+ function zeroUsage() {
24
+ return {
25
+ searchCalls: 0,
26
+ filesRead: 0,
27
+ excerptBytes: 0,
28
+ modelInputTokens: 0,
29
+ modelOutputTokens: 0,
30
+ elapsedMs: 0,
31
+ rerankCalls: 0,
32
+ };
33
+ }
34
+ function groupAtomsByPath(atoms) {
35
+ const map = new Map();
36
+ for (const atom of atoms) {
37
+ const existing = map.get(atom.scopePath);
38
+ if (existing === undefined) {
39
+ map.set(atom.scopePath, [atom]);
40
+ }
41
+ else {
42
+ existing.push(atom);
43
+ }
44
+ }
45
+ return map;
46
+ }
47
+ function deriveSelectionReason(candidate) {
48
+ const first = candidate.signals[0];
49
+ if (first === undefined) {
50
+ return "ranked candidate";
51
+ }
52
+ return `ranked by ${first.name}`;
53
+ }
54
+ function resolveRole(scopePath, editablePaths) {
55
+ return editablePaths.has(scopePath) ? "editable" : "read-only";
56
+ }
57
+ async function applyReranker(reranker, ranked, atomsByPath, budget, usage) {
58
+ // The seam is only invoked when the budget actually allows rerank calls. This keeps
59
+ // ExplorationBudget.rerankCallsMax authoritative even when a custom reranker is supplied
60
+ // and avoids billing a rerank call against a run whose budget set rerankCallsMax=0.
61
+ if (usage.rerankCalls >= budget.rerankCallsMax) {
62
+ return { ordered: ranked, reranked: false };
63
+ }
64
+ const availability = await reranker.isAvailable();
65
+ if (!availability.available) {
66
+ return { ordered: ranked, reranked: false };
67
+ }
68
+ const reordered = await reranker.rerank(ranked, atomsByPath, ranked.length);
69
+ return { ordered: reordered, reranked: true };
70
+ }
71
+ function cloneUsage(usage) {
72
+ if (usage === undefined) {
73
+ return zeroUsage();
74
+ }
75
+ return { ...usage };
76
+ }
77
+ function emptyBuildPlan(initialUsage, initialUncertainty) {
78
+ return {
79
+ files: [],
80
+ usage: cloneUsage(initialUsage),
81
+ uncertainty: [...(initialUncertainty ?? [])],
82
+ extraOmitted: [],
83
+ };
84
+ }
85
+ function compactAtomsForCandidate(atomsForPath, source, maxBytesPerExcerpt) {
86
+ const excerpts = [];
87
+ let totalBytes = 0;
88
+ for (const atom of atomsForPath) {
89
+ const rawContent = contentForAtom(atom, source);
90
+ if (rawContent === undefined) {
91
+ continue;
92
+ }
93
+ const result = compactExcerpt({ atom, rawContent, maxBytes: maxBytesPerExcerpt });
94
+ excerpts.push(result.excerpt);
95
+ totalBytes += result.bytesConsumed;
96
+ }
97
+ return { excerpts, totalBytes };
98
+ }
99
+ function lineCount(content) {
100
+ if (content.length === 0) {
101
+ return 0;
102
+ }
103
+ return content.split("\n").length;
104
+ }
105
+ function isExcerptWindowArray(source) {
106
+ return Array.isArray(source);
107
+ }
108
+ function normalizeExcerptWindows(source) {
109
+ if (typeof source === "string") {
110
+ return [{ startLine: 1, endLine: lineCount(source), content: source }];
111
+ }
112
+ if (isExcerptWindowArray(source)) {
113
+ return source;
114
+ }
115
+ return [source];
116
+ }
117
+ function coversAtom(window, atom) {
118
+ const range = atom.lineRange;
119
+ return (range === undefined || (window.startLine <= range.startLine && window.endLine >= range.endLine));
120
+ }
121
+ function sliceWindowForAtom(window, atom) {
122
+ const range = atom.lineRange;
123
+ if (range === undefined) {
124
+ return window.content;
125
+ }
126
+ const lines = window.content.split("\n");
127
+ const startIndex = Math.max(0, range.startLine - window.startLine);
128
+ const endIndex = Math.min(lines.length, range.endLine - window.startLine + 1);
129
+ if (startIndex >= endIndex) {
130
+ return "";
131
+ }
132
+ return lines.slice(startIndex, endIndex).join("\n");
133
+ }
134
+ function contentForAtom(atom, source) {
135
+ const windows = normalizeExcerptWindows(source);
136
+ const selected = windows.find((window) => coversAtom(window, atom));
137
+ if (selected === undefined) {
138
+ return undefined;
139
+ }
140
+ return sliceWindowForAtom(selected, atom);
141
+ }
142
+ function appendUsage(usage, addedBytes) {
143
+ return {
144
+ ...usage,
145
+ filesRead: usage.filesRead + 1,
146
+ excerptBytes: usage.excerptBytes + addedBytes,
147
+ };
148
+ }
149
+ function recordBudgetClip(plan, candidate, atomsForPath, nowMs) {
150
+ plan.uncertainty.push({
151
+ kind: "budget-clipped",
152
+ claim: `context pack truncated at ${candidate.scopePath}`,
153
+ // The clipped candidate is omitted from pack.files, so its atoms are not valid
154
+ // uncertainty references under the connected-context contract.
155
+ impactedAtomIds: [],
156
+ emittedAtMs: nowMs,
157
+ });
158
+ plan.extraOmitted.push({
159
+ scopePath: candidate.scopePath,
160
+ reason: "budget-exhausted",
161
+ omittedAtMs: nowMs,
162
+ });
163
+ }
164
+ function recordNoEvidence(plan, claim, nowMs) {
165
+ plan.uncertainty.push({
166
+ kind: "no-evidence",
167
+ claim,
168
+ impactedAtomIds: [],
169
+ emittedAtMs: nowMs,
170
+ });
171
+ }
172
+ function recordPreMarkedOmission(plan, candidate, nowMs) {
173
+ if (candidate.omitted === undefined) {
174
+ return false;
175
+ }
176
+ plan.extraOmitted.push({
177
+ scopePath: candidate.scopePath,
178
+ reason: candidate.omitted,
179
+ omittedAtMs: nowMs,
180
+ });
181
+ return true;
182
+ }
183
+ function processCandidate(plan, candidate, ctx) {
184
+ // Respect a pre-set omission reason from the ranker (e.g. "generated", "ignored").
185
+ // The candidate's atoms may still exist in input.atoms (the ranker only drops them from
186
+ // its kept list), but they must not enter pack.files — that would contradict the
187
+ // omission semantics of CandidateFile.omitted.
188
+ if (recordPreMarkedOmission(plan, candidate, ctx.nowMs)) {
189
+ return "continue";
190
+ }
191
+ const excerptSource = ctx.excerpts.get(candidate.scopePath);
192
+ if (excerptSource === undefined) {
193
+ recordNoEvidence(plan, `excerpt unavailable for ${candidate.scopePath}`, ctx.nowMs);
194
+ return "continue";
195
+ }
196
+ const atomsForPath = ctx.atomsByPath.get(candidate.scopePath) ?? [];
197
+ if (atomsForPath.length === 0) {
198
+ return "continue";
199
+ }
200
+ const { excerpts, totalBytes } = compactAtomsForCandidate(atomsForPath, excerptSource, ctx.maxBytesPerExcerpt);
201
+ if (excerpts.length === 0) {
202
+ recordNoEvidence(plan, `excerpt unavailable for cited ranges in ${candidate.scopePath}`, ctx.nowMs);
203
+ return "continue";
204
+ }
205
+ const checkpoint = {
206
+ atoms: atomsForPath,
207
+ budget: ctx.budget,
208
+ currentUsage: plan.usage,
209
+ };
210
+ if (!nextAtomFitsBudget(checkpoint, totalBytes).fits) {
211
+ recordBudgetClip(plan, candidate, atomsForPath, ctx.nowMs);
212
+ return "budget-clipped";
213
+ }
214
+ plan.files.push({
215
+ scopePath: candidate.scopePath,
216
+ role: resolveRole(candidate.scopePath, ctx.editablePaths),
217
+ selectionReason: deriveSelectionReason(candidate),
218
+ excerpts,
219
+ });
220
+ plan.usage = appendUsage(plan.usage, totalBytes);
221
+ return "continue";
222
+ }
223
+ function buildPlan(ordered, ctx, initialUsage, initialUncertainty) {
224
+ const plan = emptyBuildPlan(initialUsage, initialUncertainty);
225
+ for (const candidate of ordered) {
226
+ const outcome = processCandidate(plan, candidate, ctx);
227
+ if (outcome === "budget-clipped") {
228
+ return plan;
229
+ }
230
+ }
231
+ return plan;
232
+ }
233
+ function cacheConnectedExcerpt(excerpt) {
234
+ return {
235
+ atom: cacheAtom(excerpt.atom),
236
+ contentHash: sha256Hex(excerpt.content),
237
+ contentBytes: excerpt.contentBytes,
238
+ };
239
+ }
240
+ function cacheConnectedFile(file) {
241
+ return {
242
+ scopePath: file.scopePath,
243
+ role: file.role,
244
+ selectionReason: file.selectionReason,
245
+ excerpts: file.excerpts.map(cacheConnectedExcerpt),
246
+ };
247
+ }
248
+ function buildStableId(input, plan) {
249
+ const fingerprint = sha256Hex(JSON.stringify({
250
+ scope: cacheScope(input.scope),
251
+ query: cacheQuery(input.query),
252
+ files: plan.files.map(cacheConnectedFile),
253
+ omitted: [...input.omittedFromRanking, ...plan.extraOmitted].map(cacheOmitted),
254
+ uncertainty: plan.uncertainty.map(cacheUncertainty),
255
+ }));
256
+ return connectedContextPackStableId({
257
+ scopeId: input.scope.scopeId,
258
+ queryKind: input.query.kind,
259
+ queryText: input.query.text,
260
+ atomStableIds: [`pack-fp-${fingerprint}`],
261
+ });
262
+ }
263
+ function buildPack(input, plan, nowMs) {
264
+ return {
265
+ schemaVersion: CONNECTED_CONTEXT_SCHEMA_VERSION,
266
+ stableId: buildStableId(input, plan),
267
+ scope: input.scope,
268
+ query: input.query,
269
+ budget: input.budget,
270
+ usage: plan.usage,
271
+ files: plan.files,
272
+ omitted: [...input.omittedFromRanking, ...plan.extraOmitted],
273
+ uncertainty: plan.uncertainty,
274
+ emittedAtMs: nowMs,
275
+ ledgerRef: undefined,
276
+ };
277
+ }
278
+ // ─── Public facade ────────────────────────────────────────────────────────────
279
+ // Cache-key contributors that change the produced pack. Two runs with the same atoms but
280
+ // different budgets, per-excerpt caps, editable-file sets, or reranker MUST hash to
281
+ // different keys — otherwise we could serve a cached pack that violates the new budget or
282
+ // carries the wrong file roles/order.
283
+ function sha256Hex(value) {
284
+ return createHash("sha256").update(value).digest("hex");
285
+ }
286
+ function cacheLineRange(range) {
287
+ if (range === undefined) {
288
+ return undefined;
289
+ }
290
+ return { startLine: range.startLine, endLine: range.endLine };
291
+ }
292
+ function cacheScope(scope) {
293
+ return {
294
+ schemaVersion: scope.schemaVersion,
295
+ scopeId: scope.scopeId,
296
+ workspaceRoot: scope.workspaceRoot,
297
+ kind: scope.kind,
298
+ relativePaths: scope.relativePaths,
299
+ conversationId: scope.conversationId,
300
+ connectedAtMs: scope.connectedAtMs,
301
+ explicitConnection: scope.explicitConnection,
302
+ };
303
+ }
304
+ function cacheQuery(query) {
305
+ return {
306
+ kind: query.kind,
307
+ text: query.text,
308
+ caseSensitive: query.caseSensitive,
309
+ maxResults: query.maxResults,
310
+ };
311
+ }
312
+ function cacheAtom(atom) {
313
+ return {
314
+ stableId: atom.stableId,
315
+ scopePath: atom.scopePath,
316
+ lineRange: cacheLineRange(atom.lineRange),
317
+ score: atom.score,
318
+ provenance: atom.provenance,
319
+ redactionState: atom.redactionState,
320
+ ledgerRef: atom.ledgerRef,
321
+ };
322
+ }
323
+ function cacheUsage(usage) {
324
+ if (usage === undefined) {
325
+ return undefined;
326
+ }
327
+ return {
328
+ searchCalls: usage.searchCalls,
329
+ filesRead: usage.filesRead,
330
+ excerptBytes: usage.excerptBytes,
331
+ modelInputTokens: usage.modelInputTokens,
332
+ modelOutputTokens: usage.modelOutputTokens,
333
+ rerankCalls: usage.rerankCalls,
334
+ };
335
+ }
336
+ function cacheUncertainty(marker) {
337
+ return {
338
+ kind: marker.kind,
339
+ claim: marker.claim,
340
+ impactedAtomIds: marker.impactedAtomIds,
341
+ };
342
+ }
343
+ function cacheCandidate(candidate) {
344
+ return {
345
+ scopePath: candidate.scopePath,
346
+ score: candidate.score,
347
+ signals: candidate.signals.map((signal) => ({ name: signal.name, value: signal.value })),
348
+ omitted: candidate.omitted,
349
+ };
350
+ }
351
+ function cacheOmitted(entry) {
352
+ return {
353
+ scopePath: entry.scopePath,
354
+ reason: entry.reason,
355
+ };
356
+ }
357
+ function cacheExcerptWindow(window) {
358
+ return {
359
+ startLine: window.startLine,
360
+ endLine: window.endLine,
361
+ contentHash: sha256Hex(window.content),
362
+ };
363
+ }
364
+ function cacheExcerpts(excerpts) {
365
+ return [...excerpts.entries()]
366
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
367
+ .map(([scopePath, source]) => ({
368
+ scopePath,
369
+ windows: normalizeExcerptWindows(source).map(cacheExcerptWindow),
370
+ }));
371
+ }
372
+ function cacheExcerptIdentity(input) {
373
+ if (input.cacheIdentity !== undefined) {
374
+ return [...input.cacheIdentity].sort();
375
+ }
376
+ return cacheExcerpts(input.excerpts);
377
+ }
378
+ function buildCacheAtomIds(input, resolved) {
379
+ const fingerprintSource = JSON.stringify({
380
+ scope: cacheScope(input.scope),
381
+ query: cacheQuery(input.query),
382
+ atoms: input.atoms.map(cacheAtom),
383
+ budget: input.budget,
384
+ initialUsage: cacheUsage(input.initialUsage),
385
+ initialUncertainty: input.cacheIdentity === undefined
386
+ ? input.initialUncertainty?.map(cacheUncertainty)
387
+ : undefined,
388
+ ranked: input.ranked.map(cacheCandidate),
389
+ omittedFromRanking: input.omittedFromRanking.map(cacheOmitted),
390
+ excerpts: cacheExcerptIdentity(input),
391
+ maxBytesPerExcerpt: resolved.maxBytesPerExcerpt,
392
+ editablePaths: [...resolved.editablePaths].sort(),
393
+ rerankerName: resolved.reranker.name,
394
+ });
395
+ return [`fp-${sha256Hex(fingerprintSource)}`];
396
+ }
397
+ export function contextPackIndexKey(input, options) {
398
+ const resolved = resolveOptions(options);
399
+ return makeIndexKey({
400
+ scopeId: input.scope.scopeId,
401
+ queryKind: input.query.kind,
402
+ queryText: input.query.text,
403
+ atomStableIds: buildCacheAtomIds(input, resolved),
404
+ });
405
+ }
406
+ export async function assembleContextPack(input, options) {
407
+ const resolved = resolveOptions(options);
408
+ const key = contextPackIndexKey(input, options);
409
+ const cached = resolved.microIndex?.get(key);
410
+ if (cached !== undefined) {
411
+ return { pack: cached, fromIndex: true };
412
+ }
413
+ const atomsByPath = groupAtomsByPath(input.atoms);
414
+ const initialUsage = cloneUsage(input.initialUsage);
415
+ const rerankerOutcome = await applyReranker(resolved.reranker, input.ranked, atomsByPath, input.budget, initialUsage);
416
+ const now = resolved.nowMs();
417
+ const plan = buildPlan(rerankerOutcome.ordered, {
418
+ atomsByPath,
419
+ excerpts: input.excerpts,
420
+ budget: input.budget,
421
+ maxBytesPerExcerpt: resolved.maxBytesPerExcerpt,
422
+ editablePaths: resolved.editablePaths,
423
+ nowMs: now,
424
+ }, initialUsage, input.initialUncertainty);
425
+ if (rerankerOutcome.reranked) {
426
+ plan.usage = { ...plan.usage, rerankCalls: plan.usage.rerankCalls + 1 };
427
+ }
428
+ const pack = buildPack(input, plan, now);
429
+ resolved.microIndex?.set(key, pack);
430
+ return { pack, fromIndex: false };
431
+ }
@@ -0,0 +1,23 @@
1
+ import type { ContextExcerpt, EvidenceAtom, ExplorationBudget, ExplorationUsage } from "@oscharko-dev/keiko-contracts/connected-context";
2
+ export interface CompactionInput {
3
+ readonly atom: EvidenceAtom;
4
+ readonly rawContent: string;
5
+ readonly maxBytes: number;
6
+ }
7
+ export interface CompactionResult {
8
+ readonly excerpt: ContextExcerpt;
9
+ readonly bytesConsumed: number;
10
+ readonly truncated: boolean;
11
+ }
12
+ export interface BudgetCheckpoint {
13
+ readonly atoms: readonly EvidenceAtom[];
14
+ readonly budget: ExplorationBudget;
15
+ readonly currentUsage: ExplorationUsage;
16
+ }
17
+ export interface BudgetCheckpointResult {
18
+ readonly fits: boolean;
19
+ readonly violatedDim?: string;
20
+ }
21
+ export declare function compactExcerpt(input: CompactionInput): CompactionResult;
22
+ export declare function nextAtomFitsBudget(cp: BudgetCheckpoint, candidateAtomBytes: number): BudgetCheckpointResult;
23
+ //# sourceMappingURL=compaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../src/contextpack/compaction.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,cAAc,EACd,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,iDAAiD,CAAC;AAIzD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC;CACzC;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAqCD,wBAAgB,cAAc,CAAC,KAAK,EAAE,eAAe,GAAG,gBAAgB,CAiBvE;AAID,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,gBAAgB,EACpB,kBAAkB,EAAE,MAAM,GACzB,sBAAsB,CAaxB"}
@@ -0,0 +1,68 @@
1
+ // Excerpt compaction and aggregate-budget checkpoint helpers for the connected-context
2
+ // assembler (Epic #177, Issue #183). Pure functions: no IO, no clock, no randomness. The
3
+ // input `rawContent` is already redacted by the #179 search facade boundary; this module
4
+ // only clamps to UTF-8 byte budgets and reports whether the next atom would fit the
5
+ // per-pack ExplorationBudget envelope.
6
+ // ─── UTF-8 byte clamping ──────────────────────────────────────────────────────
7
+ const TEXT_ENCODER = new TextEncoder();
8
+ // `fatal: false` so a clamp that lands inside a multi-byte sequence emits U+FFFD instead
9
+ // of throwing — we then strip the replacement char to keep the excerpt boundary clean.
10
+ const TEXT_DECODER = new TextDecoder("utf-8", { fatal: false });
11
+ const REPLACEMENT_CHAR = "�";
12
+ function utf8ByteLength(value) {
13
+ return TEXT_ENCODER.encode(value).length;
14
+ }
15
+ function clampToBytes(value, maxBytes) {
16
+ if (maxBytes <= 0) {
17
+ return "";
18
+ }
19
+ const encoded = TEXT_ENCODER.encode(value);
20
+ if (encoded.length <= maxBytes) {
21
+ return value;
22
+ }
23
+ const sliced = encoded.subarray(0, maxBytes);
24
+ const decoded = TEXT_DECODER.decode(sliced);
25
+ if (!decoded.endsWith(REPLACEMENT_CHAR)) {
26
+ return decoded;
27
+ }
28
+ // Strip trailing replacement chars so the excerpt never exposes a partial code point.
29
+ let trimmed = decoded;
30
+ while (trimmed.endsWith(REPLACEMENT_CHAR)) {
31
+ trimmed = trimmed.slice(0, -1);
32
+ }
33
+ return trimmed;
34
+ }
35
+ // ─── compactExcerpt ───────────────────────────────────────────────────────────
36
+ export function compactExcerpt(input) {
37
+ if (!Number.isInteger(input.maxBytes) || input.maxBytes < 0) {
38
+ throw new RangeError("compactExcerpt: maxBytes must be a non-negative integer");
39
+ }
40
+ const originalBytes = utf8ByteLength(input.rawContent);
41
+ const clamped = clampToBytes(input.rawContent, input.maxBytes);
42
+ const contentBytes = utf8ByteLength(clamped);
43
+ const excerpt = {
44
+ atom: input.atom,
45
+ content: clamped,
46
+ contentBytes,
47
+ };
48
+ return {
49
+ excerpt,
50
+ bytesConsumed: contentBytes,
51
+ truncated: contentBytes < originalBytes,
52
+ };
53
+ }
54
+ // ─── nextAtomFitsBudget ───────────────────────────────────────────────────────
55
+ export function nextAtomFitsBudget(cp, candidateAtomBytes) {
56
+ if (!Number.isFinite(candidateAtomBytes) || candidateAtomBytes < 0) {
57
+ return { fits: false, violatedDim: "excerptBytes" };
58
+ }
59
+ const projectedBytes = cp.currentUsage.excerptBytes + candidateAtomBytes;
60
+ if (projectedBytes > cp.budget.excerptBytesMax) {
61
+ return { fits: false, violatedDim: "excerptBytes" };
62
+ }
63
+ const projectedFiles = cp.currentUsage.filesRead + 1;
64
+ if (projectedFiles > cp.budget.filesReadMax) {
65
+ return { fits: false, violatedDim: "filesRead" };
66
+ }
67
+ return { fits: true };
68
+ }
@@ -0,0 +1,9 @@
1
+ export type { BudgetCheckpoint, BudgetCheckpointResult, CompactionInput, CompactionResult, } from "./compaction.js";
2
+ export { compactExcerpt, nextAtomFitsBudget } from "./compaction.js";
3
+ export type { RerankerAvailability, RerankerSeam } from "./reranker.js";
4
+ export { disabledReranker } from "./reranker.js";
5
+ export type { IndexEntry, IndexKeyInput, MicroIndex, MicroIndexOptions } from "./microIndex.js";
6
+ export { createMicroIndex, DEFAULT_MICRO_INDEX, makeIndexKey } from "./microIndex.js";
7
+ export type { AssembleInput, AssembleOptions, AssembleResult, ExcerptSource, ExcerptWindow, } from "./assemble.js";
8
+ export { assembleContextPack, contextPackIndexKey } from "./assemble.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/contextpack/index.ts"],"names":[],"mappings":"AAKA,YAAY,EACV,gBAAgB,EAChB,sBAAsB,EACtB,eAAe,EACf,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAErE,YAAY,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjD,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAChG,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEtF,YAAY,EACV,aAAa,EACb,eAAe,EACf,cAAc,EACd,aAAa,EACb,aAAa,GACd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,8 @@
1
+ // Public sub-barrel for the ephemeral micro-index and connected-context pack assembler
2
+ // (Epic #177, Issue #183). External consumers import every contextpack symbol through
3
+ // this module; internal modules (compaction.ts, reranker.ts, microIndex.ts, assemble.ts)
4
+ // remain implementation detail.
5
+ export { compactExcerpt, nextAtomFitsBudget } from "./compaction.js";
6
+ export { disabledReranker } from "./reranker.js";
7
+ export { createMicroIndex, DEFAULT_MICRO_INDEX, makeIndexKey } from "./microIndex.js";
8
+ export { assembleContextPack, contextPackIndexKey } from "./assemble.js";
@@ -0,0 +1,29 @@
1
+ import type { ConnectedContextPack } from "@oscharko-dev/keiko-contracts/connected-context";
2
+ export interface IndexEntry {
3
+ readonly key: string;
4
+ readonly pack: ConnectedContextPack;
5
+ readonly insertedAtMs: number;
6
+ readonly expiresAtMs: number;
7
+ }
8
+ export interface MicroIndexOptions {
9
+ readonly ttlMs: number;
10
+ readonly maxEntries: number;
11
+ readonly nowMs: () => number;
12
+ }
13
+ export declare const DEFAULT_MICRO_INDEX: Omit<MicroIndexOptions, "nowMs">;
14
+ export interface MicroIndex {
15
+ get(key: string): ConnectedContextPack | undefined;
16
+ set(key: string, pack: ConnectedContextPack): void;
17
+ delete(key: string): void;
18
+ clear(): void;
19
+ size(): number;
20
+ }
21
+ export interface IndexKeyInput {
22
+ readonly scopeId: string;
23
+ readonly queryKind: string;
24
+ readonly queryText: string;
25
+ readonly atomStableIds: readonly string[];
26
+ }
27
+ export declare function makeIndexKey(input: IndexKeyInput): string;
28
+ export declare function createMicroIndex(options: MicroIndexOptions): MicroIndex;
29
+ //# sourceMappingURL=microIndex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"microIndex.d.ts","sourceRoot":"","sources":["../../src/contextpack/microIndex.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iDAAiD,CAAC;AAI5F,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,MAAM,CAAC;CAC9B;AAED,eAAO,MAAM,mBAAmB,EAAE,IAAI,CAAC,iBAAiB,EAAE,OAAO,CAGvD,CAAC;AAEX,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS,CAAC;IACnD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACnD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,IAAI,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C;AASD,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAGzD;AAqCD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU,CAoDvE"}
@@ -0,0 +1,98 @@
1
+ // Ephemeral, session-scoped micro-index for assembled ConnectedContextPacks (Epic #177,
2
+ // Issue #183). Pure in-memory cache backed by a Map; entries are TTL-bounded and capped
3
+ // by maxEntries (insertion order = LRU). NEVER written to disk — the audit ledger (#187)
4
+ // owns persistence. makeIndexKey uses SHA-256 with a fixed prefix so identical inputs
5
+ // reuse the same cached pack regardless of caller order on atomStableIds.
6
+ import { createHash } from "node:crypto";
7
+ export const DEFAULT_MICRO_INDEX = {
8
+ ttlMs: 5 * 60 * 1000,
9
+ maxEntries: 32,
10
+ };
11
+ // ─── Key derivation ───────────────────────────────────────────────────────────
12
+ function canonicalKeyInput(input) {
13
+ const sorted = [...input.atomStableIds].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
14
+ return JSON.stringify([input.scopeId, input.queryKind, input.queryText, sorted]);
15
+ }
16
+ export function makeIndexKey(input) {
17
+ const hash = createHash("sha256").update(canonicalKeyInput(input)).digest("hex");
18
+ return `ix-${hash.slice(0, 16)}`;
19
+ }
20
+ // ─── Micro-index factory ──────────────────────────────────────────────────────
21
+ function validateOptions(options) {
22
+ if (!Number.isFinite(options.ttlMs) || options.ttlMs <= 0) {
23
+ throw new RangeError("createMicroIndex: ttlMs must be a positive finite number");
24
+ }
25
+ if (!Number.isInteger(options.maxEntries) || options.maxEntries <= 0) {
26
+ throw new RangeError("createMicroIndex: maxEntries must be a positive integer");
27
+ }
28
+ // Validate eagerly so the failure mode is a TypeError at construction rather than a
29
+ // less actionable "options.nowMs is not a function" the next time get/set is called.
30
+ if (typeof options.nowMs !== "function") {
31
+ throw new TypeError("createMicroIndex: nowMs must be a function");
32
+ }
33
+ }
34
+ function evictExpired(store, nowMs) {
35
+ for (const [key, entry] of store) {
36
+ if (entry.expiresAtMs <= nowMs) {
37
+ store.delete(key);
38
+ }
39
+ }
40
+ }
41
+ function evictOldest(store, capacity) {
42
+ while (store.size >= capacity) {
43
+ const iterator = store.keys();
44
+ const next = iterator.next();
45
+ if (next.done === true) {
46
+ return;
47
+ }
48
+ store.delete(next.value);
49
+ }
50
+ }
51
+ export function createMicroIndex(options) {
52
+ validateOptions(options);
53
+ // Insertion order on a Map gives natural LRU semantics: `set` re-inserts at the tail,
54
+ // and `evictOldest` removes from the head.
55
+ const store = new Map();
56
+ function get(key) {
57
+ const entry = store.get(key);
58
+ if (entry === undefined) {
59
+ return undefined;
60
+ }
61
+ const now = options.nowMs();
62
+ if (entry.expiresAtMs <= now) {
63
+ store.delete(key);
64
+ return undefined;
65
+ }
66
+ // Refresh insertion order without changing expiry — recent reads stay warm.
67
+ store.delete(key);
68
+ store.set(key, entry);
69
+ return entry.pack;
70
+ }
71
+ function set(key, pack) {
72
+ const now = options.nowMs();
73
+ evictExpired(store, now);
74
+ if (store.has(key)) {
75
+ store.delete(key);
76
+ }
77
+ else {
78
+ evictOldest(store, options.maxEntries);
79
+ }
80
+ const entry = {
81
+ key,
82
+ pack,
83
+ insertedAtMs: now,
84
+ expiresAtMs: now + options.ttlMs,
85
+ };
86
+ store.set(key, entry);
87
+ }
88
+ function deleteKey(key) {
89
+ store.delete(key);
90
+ }
91
+ function clear() {
92
+ store.clear();
93
+ }
94
+ function size() {
95
+ return store.size;
96
+ }
97
+ return { get, set, delete: deleteKey, clear, size };
98
+ }