@oscharko-dev/keiko-workflows 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/context-budget/allocator.d.ts +29 -0
- package/dist/context-budget/allocator.d.ts.map +1 -0
- package/dist/context-budget/allocator.js +185 -0
- package/dist/context-budget/compaction-helpers.d.ts +44 -0
- package/dist/context-budget/compaction-helpers.d.ts.map +1 -0
- package/dist/context-budget/compaction-helpers.js +203 -0
- package/dist/context-budget/compaction.d.ts +14 -0
- package/dist/context-budget/compaction.d.ts.map +1 -0
- package/dist/context-budget/compaction.js +97 -0
- package/dist/context-budget/defaults.d.ts +3 -0
- package/dist/context-budget/defaults.d.ts.map +1 -0
- package/dist/context-budget/defaults.js +75 -0
- package/dist/context-budget/index.d.ts +8 -0
- package/dist/context-budget/index.d.ts.map +1 -0
- package/dist/context-budget/index.js +6 -0
- package/dist/context-budget/rehydration.d.ts +11 -0
- package/dist/context-budget/rehydration.d.ts.map +1 -0
- package/dist/context-budget/rehydration.js +76 -0
- package/dist/contextpack/assemble.d.ts +2 -1
- package/dist/contextpack/assemble.d.ts.map +1 -1
- package/dist/contextpack/assemble.js +19 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/observations/command.d.ts +6 -0
- package/dist/observations/command.d.ts.map +1 -0
- package/dist/observations/command.js +67 -0
- package/dist/observations/index.d.ts +5 -0
- package/dist/observations/index.d.ts.map +1 -0
- package/dist/observations/index.js +6 -0
- package/dist/observations/search.d.ts +9 -0
- package/dist/observations/search.d.ts.map +1 -0
- package/dist/observations/search.js +43 -0
- package/dist/observations/shared.d.ts +18 -0
- package/dist/observations/shared.d.ts.map +1 -0
- package/dist/observations/shared.js +63 -0
- package/dist/observations/test.d.ts +6 -0
- package/dist/observations/test.d.ts.map +1 -0
- package/dist/observations/test.js +47 -0
- package/dist/planner/anchors.d.ts.map +1 -1
- package/dist/planner/anchors.js +3 -1
- package/dist/planner/intent.d.ts.map +1 -1
- package/dist/planner/intent.js +6 -0
- package/dist/ranking/rank.d.ts.map +1 -1
- package/dist/ranking/rank.js +5 -3
- package/dist/ranking/scoring.d.ts +4 -0
- package/dist/ranking/scoring.d.ts.map +1 -1
- package/dist/ranking/scoring.js +29 -1
- package/dist/ranking/signals.d.ts +5 -1
- package/dist/ranking/signals.d.ts.map +1 -1
- package/dist/ranking/signals.js +23 -3
- package/package.json +10 -10
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ContextAssemblyDiagnostics, type ContextBudget, type ContextLaneDiagnostics, type ContextLaneId, type ContextProfile } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
export interface ContextLaneItemInput {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly text: string;
|
|
5
|
+
readonly score: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ContextLaneInput {
|
|
8
|
+
readonly laneId: ContextLaneId;
|
|
9
|
+
readonly items: readonly ContextLaneItemInput[];
|
|
10
|
+
}
|
|
11
|
+
export interface AllocateContextInput {
|
|
12
|
+
readonly profile: ContextProfile;
|
|
13
|
+
readonly budget: ContextBudget;
|
|
14
|
+
readonly lanes: readonly ContextLaneInput[];
|
|
15
|
+
}
|
|
16
|
+
export interface AllocatedContextLane {
|
|
17
|
+
readonly laneId: ContextLaneId;
|
|
18
|
+
readonly includedItemIds: readonly string[];
|
|
19
|
+
readonly excludedItemIds: readonly string[];
|
|
20
|
+
readonly estimatedTokens: number;
|
|
21
|
+
readonly diagnostics: ContextLaneDiagnostics;
|
|
22
|
+
}
|
|
23
|
+
export interface AllocateContextResult {
|
|
24
|
+
readonly lanes: readonly AllocatedContextLane[];
|
|
25
|
+
readonly diagnostics: ContextAssemblyDiagnostics;
|
|
26
|
+
readonly totalEstimatedTokens: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function allocateContext(input: AllocateContextInput): AllocateContextResult;
|
|
29
|
+
//# sourceMappingURL=allocator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allocator.d.ts","sourceRoot":"","sources":["../../src/context-budget/allocator.ts"],"names":[],"mappings":"AAKA,OAAO,EAIL,KAAK,0BAA0B,EAC/B,KAAK,aAAa,EAGlB,KAAK,sBAAsB,EAC3B,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,+BAA+B,CAAC;AAEvC,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,SAAS,oBAAoB,EAAE,CAAC;CACjD;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAC7C;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,WAAW,EAAE,sBAAsB,CAAC;CAC9C;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,KAAK,EAAE,SAAS,oBAAoB,EAAE,CAAC;IAChD,QAAQ,CAAC,WAAW,EAAE,0BAA0B,CAAC;IACjD,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;CACvC;AAkND,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,qBAAqB,CAoBlF"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Pure, deterministic, no-IO lane budget allocator (ADR-0052 D3/D6). Mirrors the immutable
|
|
2
|
+
// state pattern of planner/governor.ts: never mutates inputs, returns fresh objects, no clock,
|
|
3
|
+
// no randomness, no network. estimateTokens from keiko-contracts is the SINGLE token currency
|
|
4
|
+
// (ADR-0052 gate 3) — no other ratio appears in this module.
|
|
5
|
+
import { CONTEXT_ENGINEERING_SCHEMA_VERSION, CONTEXT_LANE_IDS, estimateTokens, } from "@oscharko-dev/keiko-contracts";
|
|
6
|
+
const CANONICAL_INDEX = new Map(CONTEXT_LANE_IDS.map((id, index) => [id, index]));
|
|
7
|
+
function canonicalIndex(laneId) {
|
|
8
|
+
return CANONICAL_INDEX.get(laneId) ?? CONTEXT_LANE_IDS.length;
|
|
9
|
+
}
|
|
10
|
+
// Deterministic score-greedy fill by TOKEN budget. Mirrors the proven ordering of
|
|
11
|
+
// selectScoredTextByByteBudget (packages/keiko-workspace/src/contextPack.ts:53): score DESC,
|
|
12
|
+
// then id ASC (localeCompare) for stable ties, greedy-fill until the cap is reached. Byte-reuse
|
|
13
|
+
// was rejected here because that primitive measures utf8 BYTES, which would corrupt the single
|
|
14
|
+
// token currency the allocator must use (estimateTokens) for every lane.
|
|
15
|
+
function selectScoredByTokenBudget(items, tokenBudget) {
|
|
16
|
+
const ordered = [...items].sort((a, b) => {
|
|
17
|
+
const byScore = b.score - a.score;
|
|
18
|
+
return byScore !== 0 ? byScore : a.id.localeCompare(b.id);
|
|
19
|
+
});
|
|
20
|
+
const includedIds = [];
|
|
21
|
+
const excludedIds = [];
|
|
22
|
+
let tokens = 0;
|
|
23
|
+
for (const item of ordered) {
|
|
24
|
+
const cost = estimateTokens(item.text);
|
|
25
|
+
if (tokens + cost > tokenBudget) {
|
|
26
|
+
excludedIds.push(item.id);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
includedIds.push(item.id);
|
|
30
|
+
tokens += cost;
|
|
31
|
+
}
|
|
32
|
+
return { includedIds, excludedIds, tokens, droppedForBudget: excludedIds.length > 0 };
|
|
33
|
+
}
|
|
34
|
+
// Non-evictable lanes include EVERY item (reserved off the top, never dropped) even if their
|
|
35
|
+
// total exceeds the budget — that overflow is the only permitted exception (forces "exceeded").
|
|
36
|
+
function includeAll(items) {
|
|
37
|
+
const includedIds = items.map((item) => item.id);
|
|
38
|
+
let tokens = 0;
|
|
39
|
+
for (const item of items) {
|
|
40
|
+
tokens += estimateTokens(item.text);
|
|
41
|
+
}
|
|
42
|
+
return { includedIds, excludedIds: [], tokens, droppedForBudget: false };
|
|
43
|
+
}
|
|
44
|
+
function pressureFor(tokens, cap) {
|
|
45
|
+
if (cap <= 0) {
|
|
46
|
+
return tokens > 0 ? "exceeded" : "low";
|
|
47
|
+
}
|
|
48
|
+
const ratio = tokens / cap;
|
|
49
|
+
if (ratio >= 1) {
|
|
50
|
+
return "exceeded";
|
|
51
|
+
}
|
|
52
|
+
if (ratio >= 0.85) {
|
|
53
|
+
return "high";
|
|
54
|
+
}
|
|
55
|
+
if (ratio >= 0.6) {
|
|
56
|
+
return "moderate";
|
|
57
|
+
}
|
|
58
|
+
return "low";
|
|
59
|
+
}
|
|
60
|
+
function laneDiagnostics(laneId, fill, cap) {
|
|
61
|
+
const base = {
|
|
62
|
+
laneId,
|
|
63
|
+
estimatedTokens: fill.tokens,
|
|
64
|
+
includedItems: fill.includedIds.length,
|
|
65
|
+
excludedItems: fill.excludedIds.length,
|
|
66
|
+
budgetPressure: pressureFor(fill.tokens, cap),
|
|
67
|
+
provenanceCounts: {
|
|
68
|
+
included: fill.includedIds.length,
|
|
69
|
+
excluded: fill.excludedIds.length,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
return fill.droppedForBudget ? { ...base, compactionReason: "budget" } : base;
|
|
73
|
+
}
|
|
74
|
+
function toAllocatedLane(laneId, fill, cap) {
|
|
75
|
+
return {
|
|
76
|
+
laneId,
|
|
77
|
+
includedItemIds: fill.includedIds,
|
|
78
|
+
excludedItemIds: fill.excludedIds,
|
|
79
|
+
estimatedTokens: fill.tokens,
|
|
80
|
+
diagnostics: laneDiagnostics(laneId, fill, cap),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// Joins each present input lane to its budget row. Lanes absent from input.lanes contribute
|
|
84
|
+
// nothing; an input lane with no matching budget row is ignored gracefully (no throw).
|
|
85
|
+
function planLanes(input) {
|
|
86
|
+
const itemsByLane = new Map();
|
|
87
|
+
for (const lane of input.lanes) {
|
|
88
|
+
itemsByLane.set(lane.laneId, lane.items);
|
|
89
|
+
}
|
|
90
|
+
const planned = [];
|
|
91
|
+
for (const row of input.budget.lanes) {
|
|
92
|
+
const items = itemsByLane.get(row.laneId);
|
|
93
|
+
if (items !== undefined) {
|
|
94
|
+
planned.push({ row, items });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return planned;
|
|
98
|
+
}
|
|
99
|
+
function isNonEvictable(row) {
|
|
100
|
+
return row.eviction === "none";
|
|
101
|
+
}
|
|
102
|
+
// Evictable lanes are filled in priority order (lower priority first; canonical lane order
|
|
103
|
+
// breaks ties) so eviction is deterministic and matches the ADR allocation order.
|
|
104
|
+
function byEvictionOrder(a, b) {
|
|
105
|
+
const byPriority = a.row.priority - b.row.priority;
|
|
106
|
+
return byPriority !== 0
|
|
107
|
+
? byPriority
|
|
108
|
+
: canonicalIndex(a.row.laneId) - canonicalIndex(b.row.laneId);
|
|
109
|
+
}
|
|
110
|
+
function fillEvictableLanes(lanes, budgetAfterReserved) {
|
|
111
|
+
const ordered = [...lanes].sort(byEvictionOrder);
|
|
112
|
+
const fills = new Map();
|
|
113
|
+
let remaining = Math.max(0, budgetAfterReserved);
|
|
114
|
+
for (const lane of ordered) {
|
|
115
|
+
const cap = Math.min(lane.row.maxTokens, remaining);
|
|
116
|
+
const fill = selectScoredByTokenBudget(lane.items, cap);
|
|
117
|
+
fills.set(lane.row.laneId, fill);
|
|
118
|
+
remaining -= fill.tokens;
|
|
119
|
+
}
|
|
120
|
+
return fills;
|
|
121
|
+
}
|
|
122
|
+
function buildLaneFills(planned, effectiveBudget) {
|
|
123
|
+
const fills = new Map();
|
|
124
|
+
let reservedTokens = 0;
|
|
125
|
+
let nonEvictableOverflow = false;
|
|
126
|
+
const evictable = [];
|
|
127
|
+
for (const lane of planned) {
|
|
128
|
+
if (isNonEvictable(lane.row)) {
|
|
129
|
+
const fill = includeAll(lane.items);
|
|
130
|
+
fills.set(lane.row.laneId, fill);
|
|
131
|
+
reservedTokens += fill.tokens;
|
|
132
|
+
if (fill.tokens > lane.row.maxTokens) {
|
|
133
|
+
nonEvictableOverflow = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
evictable.push(lane);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const evictableFills = fillEvictableLanes(evictable, effectiveBudget - reservedTokens);
|
|
141
|
+
for (const [laneId, fill] of evictableFills) {
|
|
142
|
+
fills.set(laneId, fill);
|
|
143
|
+
}
|
|
144
|
+
return { fills, reservedTokens, nonEvictableOverflow };
|
|
145
|
+
}
|
|
146
|
+
function capForLane(budget, laneId) {
|
|
147
|
+
for (const row of budget.lanes) {
|
|
148
|
+
if (row.laneId === laneId) {
|
|
149
|
+
return row.maxTokens;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
// Emits the allocated lanes in the canonical CONTEXT_LANE_IDS order so a later prompt assembler
|
|
155
|
+
// can place them START -> END deterministically (lost-in-the-middle layout obligation).
|
|
156
|
+
function orderResults(budget, fills) {
|
|
157
|
+
const out = [];
|
|
158
|
+
for (const laneId of CONTEXT_LANE_IDS) {
|
|
159
|
+
const fill = fills.get(laneId);
|
|
160
|
+
if (fill !== undefined) {
|
|
161
|
+
out.push(toAllocatedLane(laneId, fill, capForLane(budget, laneId)));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
export function allocateContext(input) {
|
|
167
|
+
const effectiveBudget = input.profile.effectiveInputBudget;
|
|
168
|
+
const planned = planLanes(input);
|
|
169
|
+
const { fills, reservedTokens, nonEvictableOverflow } = buildLaneFills(planned, effectiveBudget);
|
|
170
|
+
const lanes = orderResults(input.budget, fills);
|
|
171
|
+
const totalEstimatedTokens = lanes.reduce((sum, lane) => sum + lane.estimatedTokens, 0);
|
|
172
|
+
const overflow = nonEvictableOverflow || reservedTokens > effectiveBudget;
|
|
173
|
+
const budgetPressure = overflow
|
|
174
|
+
? "exceeded"
|
|
175
|
+
: pressureFor(totalEstimatedTokens, effectiveBudget);
|
|
176
|
+
const diagnostics = {
|
|
177
|
+
schemaVersion: CONTEXT_ENGINEERING_SCHEMA_VERSION,
|
|
178
|
+
profile: input.profile,
|
|
179
|
+
totalEstimatedTokens,
|
|
180
|
+
budgetPressure,
|
|
181
|
+
lanes: lanes.map((lane) => lane.diagnostics),
|
|
182
|
+
orderedForRecency: true,
|
|
183
|
+
};
|
|
184
|
+
return { lanes, diagnostics, totalEstimatedTokens };
|
|
185
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type ContextAssumption, type ContextCommandOutcome, type ContextInvalidationKey, type ContextLaneId, type ContextPreservedFact, type ContextProvenanceRef, type ContextRehydrationHandle, type ContextUserConstraint } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
import type { ContextLaneInput, ContextLaneItemInput } from "./allocator.js";
|
|
3
|
+
export interface CompactionDigest {
|
|
4
|
+
readonly preservedFacts?: readonly ContextPreservedFact[] | undefined;
|
|
5
|
+
readonly assumptions?: readonly ContextAssumption[] | undefined;
|
|
6
|
+
readonly userConstraints?: readonly ContextUserConstraint[] | undefined;
|
|
7
|
+
readonly decisions?: readonly string[] | undefined;
|
|
8
|
+
readonly openQuestions?: readonly string[] | undefined;
|
|
9
|
+
readonly filesInspected?: readonly string[] | undefined;
|
|
10
|
+
readonly filesChanged?: readonly string[] | undefined;
|
|
11
|
+
readonly commandOutcomes?: readonly ContextCommandOutcome[] | undefined;
|
|
12
|
+
readonly failingTests?: readonly string[] | undefined;
|
|
13
|
+
readonly droppedCategories?: readonly string[] | undefined;
|
|
14
|
+
}
|
|
15
|
+
export interface DigestProjection {
|
|
16
|
+
readonly preservedFacts?: readonly ContextPreservedFact[] | undefined;
|
|
17
|
+
readonly assumptions?: readonly ContextAssumption[] | undefined;
|
|
18
|
+
readonly userConstraints?: readonly ContextUserConstraint[] | undefined;
|
|
19
|
+
readonly decisions?: readonly string[] | undefined;
|
|
20
|
+
readonly openQuestions?: readonly string[] | undefined;
|
|
21
|
+
readonly filesInspected?: readonly string[] | undefined;
|
|
22
|
+
readonly filesChanged?: readonly string[] | undefined;
|
|
23
|
+
readonly commandOutcomes?: readonly ContextCommandOutcome[] | undefined;
|
|
24
|
+
readonly failingTests?: readonly string[] | undefined;
|
|
25
|
+
readonly droppedCategories?: readonly string[] | undefined;
|
|
26
|
+
}
|
|
27
|
+
export declare function projectFacts(facts: readonly ContextPreservedFact[]): {
|
|
28
|
+
readonly kept: readonly ContextPreservedFact[];
|
|
29
|
+
readonly dropped: number;
|
|
30
|
+
};
|
|
31
|
+
export declare function projectDigest(digest: CompactionDigest): DigestProjection;
|
|
32
|
+
export declare function tokensForItems(items: readonly ContextLaneItemInput[]): number;
|
|
33
|
+
export declare function laneItems(lanes: readonly ContextLaneInput[], laneId: ContextLaneId): readonly ContextLaneItemInput[];
|
|
34
|
+
export declare function itemsByIds(items: readonly ContextLaneItemInput[], ids: readonly string[]): readonly ContextLaneItemInput[];
|
|
35
|
+
export declare function buildSourceSpans(excluded: readonly ContextLaneItemInput[], provenance: ReadonlyMap<string, ContextProvenanceRef>): readonly ContextProvenanceRef[];
|
|
36
|
+
export declare function buildInvalidationKeys(spans: readonly ContextProvenanceRef[], fileHashes: ReadonlyMap<string, string> | undefined): readonly ContextInvalidationKey[];
|
|
37
|
+
export declare function buildRehydrationHandle(input: {
|
|
38
|
+
readonly laneId: ContextLaneId;
|
|
39
|
+
readonly excludedIds: readonly string[];
|
|
40
|
+
readonly spans: readonly ContextProvenanceRef[];
|
|
41
|
+
readonly approxTokens: number;
|
|
42
|
+
}): ContextRehydrationHandle;
|
|
43
|
+
export declare function summaryHashOf(excluded: readonly ContextLaneItemInput[]): string;
|
|
44
|
+
//# sourceMappingURL=compaction-helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compaction-helpers.d.ts","sourceRoot":"","sources":["../../src/context-budget/compaction-helpers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Pure helpers for the compaction builder (ADR-0053 D4). No IO, no clock, no randomness. Every
|
|
2
|
+
// free-text string that enters a record is scrubbed via redact() from keiko-security BEFORE
|
|
3
|
+
// inclusion (the PR2 in-memory defense-in-depth; the CRITICAL BOUNDARY CORRECTION over ADR-0053
|
|
4
|
+
// keeps keiko-workflows free of any keiko-memory-capture edge). Split from compaction.ts to hold
|
|
5
|
+
// both files under the 400-LOC budget.
|
|
6
|
+
import { CONTEXT_ENGINEERING_SCHEMA_VERSION, estimateTokens, validateContextPreservedFact, } from "@oscharko-dev/keiko-contracts";
|
|
7
|
+
import { hashExcerptContent } from "@oscharko-dev/keiko-workspace";
|
|
8
|
+
import { redact } from "@oscharko-dev/keiko-security";
|
|
9
|
+
function redactStrings(values) {
|
|
10
|
+
return values === undefined ? undefined : values.map((value) => redact(value));
|
|
11
|
+
}
|
|
12
|
+
function redactRef(ref) {
|
|
13
|
+
return ref.notPersistedReason === undefined
|
|
14
|
+
? ref
|
|
15
|
+
: { ...ref, notPersistedReason: redact(ref.notPersistedReason) };
|
|
16
|
+
}
|
|
17
|
+
// A redacted fact is kept ONLY when it still satisfies the sourceRef-or-inferred invariant. An
|
|
18
|
+
// unsourced, non-inferred "fact" is DROPPED (and counted by the caller) — never silently coerced.
|
|
19
|
+
function projectFact(fact) {
|
|
20
|
+
const redacted = {
|
|
21
|
+
...fact,
|
|
22
|
+
statement: redact(fact.statement),
|
|
23
|
+
...(fact.sourceRef !== undefined ? { sourceRef: redactRef(fact.sourceRef) } : {}),
|
|
24
|
+
...(fact.corroborating !== undefined
|
|
25
|
+
? { corroborating: fact.corroborating.map(redactRef) }
|
|
26
|
+
: {}),
|
|
27
|
+
};
|
|
28
|
+
return validateContextPreservedFact(redacted).ok ? redacted : undefined;
|
|
29
|
+
}
|
|
30
|
+
export function projectFacts(facts) {
|
|
31
|
+
const kept = [];
|
|
32
|
+
let dropped = 0;
|
|
33
|
+
for (const fact of facts) {
|
|
34
|
+
const projected = projectFact(fact);
|
|
35
|
+
if (projected === undefined) {
|
|
36
|
+
dropped += 1;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
kept.push(projected);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { kept, dropped };
|
|
43
|
+
}
|
|
44
|
+
function projectAssumption(assumption) {
|
|
45
|
+
return {
|
|
46
|
+
...assumption,
|
|
47
|
+
statement: redact(assumption.statement),
|
|
48
|
+
rationale: redact(assumption.rationale),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function projectConstraint(constraint) {
|
|
52
|
+
return {
|
|
53
|
+
...constraint,
|
|
54
|
+
statement: redact(constraint.statement),
|
|
55
|
+
...(constraint.sourceRef !== undefined ? { sourceRef: redactRef(constraint.sourceRef) } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function projectOutcome(outcome) {
|
|
59
|
+
return {
|
|
60
|
+
...outcome,
|
|
61
|
+
command: redact(outcome.command),
|
|
62
|
+
summary: redact(outcome.summary),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// The structured (typed) digest members: facts validated + redacted, assumptions/constraints/
|
|
66
|
+
// outcomes redacted in their free-text members. Facts stay strictly separate from assumptions.
|
|
67
|
+
function projectStructured(digest) {
|
|
68
|
+
return {
|
|
69
|
+
...(digest.preservedFacts !== undefined
|
|
70
|
+
? { preservedFacts: projectFacts(digest.preservedFacts).kept }
|
|
71
|
+
: {}),
|
|
72
|
+
...(digest.assumptions !== undefined
|
|
73
|
+
? { assumptions: digest.assumptions.map(projectAssumption) }
|
|
74
|
+
: {}),
|
|
75
|
+
...(digest.userConstraints !== undefined
|
|
76
|
+
? { userConstraints: digest.userConstraints.map(projectConstraint) }
|
|
77
|
+
: {}),
|
|
78
|
+
...(digest.commandOutcomes !== undefined
|
|
79
|
+
? { commandOutcomes: digest.commandOutcomes.map(projectOutcome) }
|
|
80
|
+
: {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// The free-text / path-list digest members. decisions/openQuestions/failingTests are redacted;
|
|
84
|
+
// file-path lists are kept verbatim (paths are not free text and never reach the browser; PR5
|
|
85
|
+
// applies the persistence-time redaction posture).
|
|
86
|
+
function projectPlain(digest) {
|
|
87
|
+
return {
|
|
88
|
+
...(digest.decisions !== undefined ? { decisions: redactStrings(digest.decisions) } : {}),
|
|
89
|
+
...(digest.openQuestions !== undefined
|
|
90
|
+
? { openQuestions: redactStrings(digest.openQuestions) }
|
|
91
|
+
: {}),
|
|
92
|
+
...(digest.filesInspected !== undefined ? { filesInspected: digest.filesInspected } : {}),
|
|
93
|
+
...(digest.filesChanged !== undefined ? { filesChanged: digest.filesChanged } : {}),
|
|
94
|
+
...(digest.failingTests !== undefined
|
|
95
|
+
? { failingTests: redactStrings(digest.failingTests) }
|
|
96
|
+
: {}),
|
|
97
|
+
...(digest.droppedCategories !== undefined
|
|
98
|
+
? { droppedCategories: digest.droppedCategories }
|
|
99
|
+
: {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Projects the digest into the additive record fields: every free-text member is redacted, facts
|
|
103
|
+
// are validated (unsourced facts dropped), assumptions stay strictly separate from facts.
|
|
104
|
+
export function projectDigest(digest) {
|
|
105
|
+
return { ...projectStructured(digest), ...projectPlain(digest) };
|
|
106
|
+
}
|
|
107
|
+
export function tokensForItems(items) {
|
|
108
|
+
let total = 0;
|
|
109
|
+
for (const item of items) {
|
|
110
|
+
total += estimateTokens(item.text);
|
|
111
|
+
}
|
|
112
|
+
return total;
|
|
113
|
+
}
|
|
114
|
+
export function laneItems(lanes, laneId) {
|
|
115
|
+
for (const lane of lanes) {
|
|
116
|
+
if (lane.laneId === laneId) {
|
|
117
|
+
return lane.items;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
export function itemsByIds(items, ids) {
|
|
123
|
+
const byId = new Map(items.map((item) => [item.id, item]));
|
|
124
|
+
const out = [];
|
|
125
|
+
for (const id of ids) {
|
|
126
|
+
const found = byId.get(id);
|
|
127
|
+
if (found !== undefined) {
|
|
128
|
+
out.push(found);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
// Enriches a repo-file ref with the per-excerpt content hash of the EXACT excluded text (the finer
|
|
134
|
+
// invalidation signal, ADR-0053 REFINEMENT 2). Non-repo-file refs are returned unchanged.
|
|
135
|
+
function enrichRef(ref, text) {
|
|
136
|
+
if (ref.kind !== "repo-file" || text === undefined) {
|
|
137
|
+
return ref;
|
|
138
|
+
}
|
|
139
|
+
return { ...ref, contentHash: hashExcerptContent(text) };
|
|
140
|
+
}
|
|
141
|
+
export function buildSourceSpans(excluded, provenance) {
|
|
142
|
+
const spans = [];
|
|
143
|
+
for (const item of excluded) {
|
|
144
|
+
const ref = provenance.get(item.id);
|
|
145
|
+
if (ref !== undefined) {
|
|
146
|
+
spans.push(enrichRef(ref, item.text));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return spans;
|
|
150
|
+
}
|
|
151
|
+
export function buildInvalidationKeys(spans, fileHashes) {
|
|
152
|
+
if (fileHashes === undefined) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
const keys = [];
|
|
156
|
+
const seen = new Set();
|
|
157
|
+
for (const span of spans) {
|
|
158
|
+
const scopePath = span.scopePath;
|
|
159
|
+
if (span.kind !== "repo-file" || scopePath === undefined || seen.has(scopePath)) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const contentHash = fileHashes.get(scopePath);
|
|
163
|
+
if (contentHash !== undefined) {
|
|
164
|
+
seen.add(scopePath);
|
|
165
|
+
keys.push({ scopePath, contentHash });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return keys;
|
|
169
|
+
}
|
|
170
|
+
// Stable handle id: hashExcerptContent over the laneId plus the SORTED excluded ids, so the same
|
|
171
|
+
// eviction set always yields the same opaque key regardless of allocator ordering.
|
|
172
|
+
function handleId(laneId, excludedIds) {
|
|
173
|
+
const sorted = [...excludedIds].sort((a, b) => (a < b ? -1 : 1));
|
|
174
|
+
return hashExcerptContent(`${laneId}:${sorted.join(",")}`);
|
|
175
|
+
}
|
|
176
|
+
// Copies the kind/scopePath/lineRange/contentHash of a single repo-file excerpt onto the handle so
|
|
177
|
+
// rehydrateHandle can resolve it directly. Only when EXACTLY one excluded item is a repo-file span.
|
|
178
|
+
function singleRepoFileFields(spans) {
|
|
179
|
+
const repoFiles = spans.filter((span) => span.kind === "repo-file");
|
|
180
|
+
const only = repoFiles.length === 1 ? repoFiles[0] : undefined;
|
|
181
|
+
if (only === undefined) {
|
|
182
|
+
return {};
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
kind: only.kind,
|
|
186
|
+
...(only.scopePath !== undefined ? { scopePath: only.scopePath } : {}),
|
|
187
|
+
...(only.lineRange !== undefined ? { lineRange: only.lineRange } : {}),
|
|
188
|
+
...(only.contentHash !== undefined ? { contentHash: only.contentHash } : {}),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
export function buildRehydrationHandle(input) {
|
|
192
|
+
return {
|
|
193
|
+
schemaVersion: CONTEXT_ENGINEERING_SCHEMA_VERSION,
|
|
194
|
+
laneId: input.laneId,
|
|
195
|
+
handleId: handleId(input.laneId, input.excludedIds),
|
|
196
|
+
itemCount: input.excludedIds.length,
|
|
197
|
+
approxTokens: input.approxTokens,
|
|
198
|
+
...singleRepoFileFields(input.spans),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
export function summaryHashOf(excluded) {
|
|
202
|
+
return hashExcerptContent(excluded.map((item) => item.text).join("\n"));
|
|
203
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ContextCompactionRecord, type ContextProvenanceRef } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
import type { AllocateContextResult, ContextLaneInput } from "./allocator.js";
|
|
3
|
+
import { type CompactionDigest } from "./compaction-helpers.js";
|
|
4
|
+
export type { CompactionDigest } from "./compaction-helpers.js";
|
|
5
|
+
export interface BuildCompactionInput {
|
|
6
|
+
readonly result: AllocateContextResult;
|
|
7
|
+
readonly lanes: readonly ContextLaneInput[];
|
|
8
|
+
readonly provenance: ReadonlyMap<string, ContextProvenanceRef>;
|
|
9
|
+
readonly orderedAt: number;
|
|
10
|
+
readonly fileHashes?: ReadonlyMap<string, string> | undefined;
|
|
11
|
+
readonly digest?: CompactionDigest | undefined;
|
|
12
|
+
}
|
|
13
|
+
export declare function buildCompactionRecords(input: BuildCompactionInput): readonly ContextCompactionRecord[];
|
|
14
|
+
//# sourceMappingURL=compaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../src/context-budget/compaction.ts"],"names":[],"mappings":"AAMA,OAAO,EAGL,KAAK,uBAAuB,EAG5B,KAAK,oBAAoB,EAE1B,MAAM,+BAA+B,CAAC;AAEvC,OAAO,KAAK,EAAE,qBAAqB,EAAwB,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACpG,OAAO,EASL,KAAK,gBAAgB,EAEtB,MAAM,yBAAyB,CAAC;AAEjC,YAAY,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAC;IACvC,QAAQ,CAAC,KAAK,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC5C,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;IAC/D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IAC9D,QAAQ,CAAC,MAAM,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;CAChD;AAkHD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,oBAAoB,GAC1B,SAAS,uBAAuB,EAAE,CAWpC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Pure, deterministic, no-IO compaction builder (ADR-0053 D4/D7). Consumes the allocator's
|
|
2
|
+
// excludedItemIds and emits one ContextCompactionRecord per evicted lane. No clock, no randomness:
|
|
3
|
+
// orderedAt is caller-injected, every hash is deterministic over the same bytes, so running the
|
|
4
|
+
// builder twice on the same input yields a deeply-equal result. Secret scrubbing uses redact()
|
|
5
|
+
// from keiko-security (NOT keiko-memory-capture — that would add a forbidden package-graph edge).
|
|
6
|
+
import { CONTEXT_ENGINEERING_SCHEMA_VERSION, validateContextCompactionRecord, } from "@oscharko-dev/keiko-contracts";
|
|
7
|
+
import { buildInvalidationKeys, buildRehydrationHandle, buildSourceSpans, itemsByIds, laneItems, projectDigest, summaryHashOf, tokensForItems, } from "./compaction-helpers.js";
|
|
8
|
+
const HISTORY_SUMMARY_LANE = "history-summary";
|
|
9
|
+
function reasonFor(lane) {
|
|
10
|
+
return lane.diagnostics.compactionReason ?? "budget";
|
|
11
|
+
}
|
|
12
|
+
function buildCore(input, lane, index) {
|
|
13
|
+
const allItems = laneItems(input.lanes, lane.laneId);
|
|
14
|
+
const included = itemsByIds(allItems, lane.includedItemIds);
|
|
15
|
+
const excluded = itemsByIds(allItems, lane.excludedItemIds);
|
|
16
|
+
const spans = buildSourceSpans(excluded, input.provenance);
|
|
17
|
+
const tokensBefore = tokensForItems(allItems);
|
|
18
|
+
const tokensAfter = tokensForItems(included);
|
|
19
|
+
return {
|
|
20
|
+
laneId: lane.laneId,
|
|
21
|
+
reason: reasonFor(lane),
|
|
22
|
+
itemsBefore: allItems.length,
|
|
23
|
+
itemsAfter: included.length,
|
|
24
|
+
tokensBefore,
|
|
25
|
+
tokensAfter,
|
|
26
|
+
summaryRefHash: summaryHashOf(excluded),
|
|
27
|
+
orderedAt: input.orderedAt + index,
|
|
28
|
+
sourceSpans: spans,
|
|
29
|
+
invalidationKeys: buildInvalidationKeys(spans, input.fileHashes),
|
|
30
|
+
rehydration: buildRehydrationHandle({
|
|
31
|
+
laneId: lane.laneId,
|
|
32
|
+
excludedIds: lane.excludedItemIds,
|
|
33
|
+
spans,
|
|
34
|
+
approxTokens: tokensBefore - tokensAfter,
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function withDigest(record, projection) {
|
|
39
|
+
return { ...record, ...projection };
|
|
40
|
+
}
|
|
41
|
+
function assembleRecord(core) {
|
|
42
|
+
const record = {
|
|
43
|
+
schemaVersion: CONTEXT_ENGINEERING_SCHEMA_VERSION,
|
|
44
|
+
laneId: core.laneId,
|
|
45
|
+
reason: core.reason,
|
|
46
|
+
itemsBefore: core.itemsBefore,
|
|
47
|
+
itemsAfter: core.itemsAfter,
|
|
48
|
+
tokensBefore: core.tokensBefore,
|
|
49
|
+
tokensAfter: core.tokensAfter,
|
|
50
|
+
summaryRefHash: core.summaryRefHash,
|
|
51
|
+
rehydration: core.rehydration,
|
|
52
|
+
orderedAt: core.orderedAt,
|
|
53
|
+
sourceSpans: core.sourceSpans,
|
|
54
|
+
...(core.invalidationKeys.length > 0 ? { invalidationKeys: core.invalidationKeys } : {}),
|
|
55
|
+
};
|
|
56
|
+
return record;
|
|
57
|
+
}
|
|
58
|
+
function digestOnlyRecord(input, projection) {
|
|
59
|
+
const record = {
|
|
60
|
+
schemaVersion: CONTEXT_ENGINEERING_SCHEMA_VERSION,
|
|
61
|
+
laneId: HISTORY_SUMMARY_LANE,
|
|
62
|
+
reason: "summarize-then-drop",
|
|
63
|
+
itemsBefore: 0,
|
|
64
|
+
itemsAfter: 0,
|
|
65
|
+
tokensBefore: 0,
|
|
66
|
+
tokensAfter: 0,
|
|
67
|
+
orderedAt: input.orderedAt,
|
|
68
|
+
...projection,
|
|
69
|
+
};
|
|
70
|
+
return record;
|
|
71
|
+
}
|
|
72
|
+
function evictedLanes(result) {
|
|
73
|
+
return result.lanes.filter((lane) => lane.excludedItemIds.length > 0);
|
|
74
|
+
}
|
|
75
|
+
function assertValid(record) {
|
|
76
|
+
const validation = validateContextCompactionRecord(record);
|
|
77
|
+
if (!validation.ok) {
|
|
78
|
+
throw new Error(`buildCompactionRecords produced an invalid record: ${validation.reasons.join(", ")}`);
|
|
79
|
+
}
|
|
80
|
+
return record;
|
|
81
|
+
}
|
|
82
|
+
// Builds one ContextCompactionRecord per evicted lane (in allocator order). The optional digest is
|
|
83
|
+
// attached to the FIRST emitted record; if no lane evicted but a digest is supplied, a single
|
|
84
|
+
// digest-only history-summary record is emitted instead. Throws if any produced record fails
|
|
85
|
+
// validation (a builder bug). Pure and idempotent.
|
|
86
|
+
export function buildCompactionRecords(input) {
|
|
87
|
+
const lanes = evictedLanes(input.result);
|
|
88
|
+
const projection = input.digest === undefined ? undefined : projectDigest(input.digest);
|
|
89
|
+
if (lanes.length === 0) {
|
|
90
|
+
return projection === undefined ? [] : [assertValid(digestOnlyRecord(input, projection))];
|
|
91
|
+
}
|
|
92
|
+
return lanes.map((lane, index) => {
|
|
93
|
+
const base = assembleRecord(buildCore(input, lane, index));
|
|
94
|
+
const enriched = index === 0 && projection !== undefined ? withDigest(base, projection) : base;
|
|
95
|
+
return assertValid(enriched);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/context-budget/defaults.ts"],"names":[],"mappings":"AAYA,OAAO,EAGL,KAAK,aAAa,EAEnB,MAAM,+BAA+B,CAAC;AA6DvC,eAAO,MAAM,sBAAsB,EAAE,aAI3B,CAAC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Default lane budget plan for the deterministic context allocator (ADR-0052 D3, "Default lane
|
|
2
|
+
// budget"). One ContextLaneBudget row per the eight lanes, pinned against DEFAULT_CONTEXT_PROFILE
|
|
3
|
+
// (effectiveInputBudget 116_000). The CONTRACT is the shape and the allocation order encoded by
|
|
4
|
+
// `priority`; the specific integers are TUNABLE and may be re-balanced without any surface change.
|
|
5
|
+
//
|
|
6
|
+
// Allocation order (priority, lower = earlier): system-contract=0, user-task=1, active-plan=2,
|
|
7
|
+
// working-memory=3, repo-evidence=4, tool-observations=5, history-summary=6, verification-evidence=7.
|
|
8
|
+
// system-contract + user-task are non-evictable (eviction "none", minReservedTokens > 0) and are
|
|
9
|
+
// reserved off the top. Per-lane maxTokens deliberately SUM ABOVE 116_000 — lanes compete and the
|
|
10
|
+
// allocator enforces effectiveInputBudget at runtime. The non-evictable minReserved sums
|
|
11
|
+
// (4_000 + 8_000 = 12_000) stay well under 116_000.
|
|
12
|
+
import { CONTEXT_ENGINEERING_SCHEMA_VERSION, DEFAULT_CONTEXT_PROFILE, } from "@oscharko-dev/keiko-contracts";
|
|
13
|
+
const DEFAULT_LANES = [
|
|
14
|
+
{
|
|
15
|
+
laneId: "system-contract",
|
|
16
|
+
priority: 0,
|
|
17
|
+
maxTokens: 6_000,
|
|
18
|
+
minReservedTokens: 4_000,
|
|
19
|
+
eviction: "none",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
laneId: "user-task",
|
|
23
|
+
priority: 1,
|
|
24
|
+
maxTokens: 12_000,
|
|
25
|
+
minReservedTokens: 8_000,
|
|
26
|
+
eviction: "none",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
laneId: "active-plan",
|
|
30
|
+
priority: 2,
|
|
31
|
+
maxTokens: 16_000,
|
|
32
|
+
minReservedTokens: 0,
|
|
33
|
+
eviction: "summarize-then-drop",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
laneId: "working-memory",
|
|
37
|
+
priority: 3,
|
|
38
|
+
maxTokens: 24_000,
|
|
39
|
+
minReservedTokens: 0,
|
|
40
|
+
eviction: "summarize-then-drop",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
laneId: "repo-evidence",
|
|
44
|
+
priority: 4,
|
|
45
|
+
maxTokens: 48_000,
|
|
46
|
+
minReservedTokens: 0,
|
|
47
|
+
eviction: "drop-lowest-score",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
laneId: "tool-observations",
|
|
51
|
+
priority: 5,
|
|
52
|
+
maxTokens: 32_000,
|
|
53
|
+
minReservedTokens: 0,
|
|
54
|
+
eviction: "drop-oldest",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
laneId: "history-summary",
|
|
58
|
+
priority: 6,
|
|
59
|
+
maxTokens: 16_000,
|
|
60
|
+
minReservedTokens: 0,
|
|
61
|
+
eviction: "summarize-then-drop",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
laneId: "verification-evidence",
|
|
65
|
+
priority: 7,
|
|
66
|
+
maxTokens: 16_000,
|
|
67
|
+
minReservedTokens: 0,
|
|
68
|
+
eviction: "drop-oldest",
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
export const DEFAULT_CONTEXT_BUDGET = {
|
|
72
|
+
schemaVersion: CONTEXT_ENGINEERING_SCHEMA_VERSION,
|
|
73
|
+
profile: DEFAULT_CONTEXT_PROFILE,
|
|
74
|
+
lanes: DEFAULT_LANES,
|
|
75
|
+
};
|