@ryuenn3123/agentic-senior-core 2.0.25 → 2.0.27

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 (37) hide show
  1. package/.agent-context/review-checklists/frontend-excellence-rubric.md +54 -0
  2. package/.agent-context/review-checklists/frontend-skill-parity.md +1 -0
  3. package/.agent-context/review-checklists/frontend-usability.md +1 -0
  4. package/.agent-context/rules/docker-runtime.md +29 -0
  5. package/.agent-context/skills/frontend/README.md +1 -0
  6. package/.agent-context/skills/frontend.md +4 -0
  7. package/.agent-context/state/benchmark-evidence-bundle.json +672 -22
  8. package/.agent-context/state/benchmark-history.json +75 -0
  9. package/.agent-context/state/benchmark-trend-report.csv +5 -0
  10. package/.agent-context/state/benchmark-trend-report.json +140 -0
  11. package/.agent-context/state/benchmark-watchlist.json +3 -3
  12. package/.agent-context/state/memory-adapter-contract.json +52 -0
  13. package/.agent-context/state/memory-continuity-benchmark.json +132 -0
  14. package/.agent-context/state/memory-schema-v1.json +88 -0
  15. package/.cursorrules +1 -1
  16. package/.windsurfrules +1 -1
  17. package/README.md +29 -0
  18. package/lib/cli/commands/init.mjs +358 -16
  19. package/lib/cli/commands/optimize.mjs +12 -0
  20. package/lib/cli/commands/upgrade.mjs +30 -1
  21. package/lib/cli/compiler.mjs +55 -1
  22. package/lib/cli/constants.mjs +83 -0
  23. package/lib/cli/detector.mjs +11 -1
  24. package/lib/cli/memory-continuity.mjs +266 -0
  25. package/lib/cli/project-scaffolder.mjs +174 -1
  26. package/lib/cli/skill-selector.mjs +60 -38
  27. package/lib/cli/templates/architecture-decision-record.md.tmpl +39 -0
  28. package/lib/cli/templates/flow-overview.md.tmpl +12 -0
  29. package/lib/cli/templates/project-brief.md.id.tmpl +2 -0
  30. package/lib/cli/templates/project-brief.md.tmpl +26 -0
  31. package/lib/cli/utils.mjs +2 -1
  32. package/package.json +2 -1
  33. package/scripts/benchmark-evidence-bundle.mjs +493 -16
  34. package/scripts/frontend-usability-audit.mjs +21 -0
  35. package/scripts/memory-continuity-benchmark.mjs +322 -0
  36. package/scripts/release-gate.mjs +30 -0
  37. package/scripts/validate.mjs +5 -0
@@ -9,6 +9,7 @@ import {
9
9
  CLI_VERSION,
10
10
  POLICY_FILE_NAME,
11
11
  SKILL_PLATFORM_INDEX_PATH,
12
+ BLUEPRINT_RECOMMENDATIONS,
12
13
  } from './constants.mjs';
13
14
 
14
15
  import {
@@ -38,12 +39,15 @@ export async function writeOnboardingReport({
38
39
  selectedProfilePack,
39
40
  selectedPreset,
40
41
  selectedStackFileName,
42
+ selectedAdditionalStackFileNames = [],
41
43
  selectedBlueprintFileName,
44
+ selectedAdditionalBlueprintFileNames = [],
42
45
  includeCiGuardrails,
43
46
  setupDurationMs,
44
47
  projectDetection,
45
48
  selectedSkillDomains = [],
46
49
  compatibilityWarnings = [],
50
+ runtimeEnvironment = null,
47
51
  operationMode = 'init',
48
52
  }) {
49
53
  const onboardingReportPath = path.join(targetDirectoryPath, '.agent-context', 'state', 'onboarding-report.json');
@@ -60,14 +64,23 @@ export async function writeOnboardingReport({
60
64
  : null,
61
65
  selectedPreset,
62
66
  selectedStack: selectedStackFileName,
67
+ selectedAdditionalStacks: selectedAdditionalStackFileNames,
63
68
  selectedBlueprint: selectedBlueprintFileName,
69
+ selectedAdditionalBlueprints: selectedAdditionalBlueprintFileNames,
64
70
  ciGuardrailsEnabled: includeCiGuardrails,
65
71
  setupDurationMs,
66
72
  selectedSkillDomains,
67
73
  compatibilityWarnings,
74
+ runtimeEnvironment,
68
75
  autoDetection: {
69
76
  recommendedStack: projectDetection.recommendedStackFileName,
77
+ recommendedAdditionalStacks: projectDetection.secondaryStackFileNames || [],
70
78
  recommendedBlueprint: projectDetection.recommendedBlueprintFileName,
79
+ recommendedAdditionalBlueprints: Array.isArray(projectDetection.secondaryStackFileNames)
80
+ ? projectDetection.secondaryStackFileNames
81
+ .map((secondaryStackFileName) => BLUEPRINT_RECOMMENDATIONS[secondaryStackFileName] || null)
82
+ .filter(Boolean)
83
+ : [],
71
84
  confidenceLabel: projectDetection.confidenceLabel,
72
85
  confidenceScore: projectDetection.confidenceScore,
73
86
  confidenceGap: projectDetection.confidenceGap,
@@ -94,7 +107,9 @@ export async function buildCompiledRulesContent({
94
107
  targetDirectoryPath,
95
108
  selectedProfileName,
96
109
  selectedStackFileName,
110
+ selectedAdditionalStackFileNames = [],
97
111
  selectedBlueprintFileName,
112
+ selectedAdditionalBlueprintFileNames = [],
98
113
  includeCiGuardrails,
99
114
  }) {
100
115
  const resolvedTargetDirectoryPath = path.resolve(targetDirectoryPath);
@@ -102,7 +117,20 @@ export async function buildCompiledRulesContent({
102
117
  const selectedStacksDirectoryPath = path.join(resolvedTargetDirectoryPath, '.agent-context', 'stacks');
103
118
  const selectedBlueprintsDirectoryPath = path.join(resolvedTargetDirectoryPath, '.agent-context', 'blueprints');
104
119
  const skillPlatformIndex = JSON.parse(await fs.readFile(SKILL_PLATFORM_INDEX_PATH, 'utf8'));
105
- const selectedSkillDomainNames = inferSkillDomainNamesFromSelection(selectedStackFileName, selectedBlueprintFileName);
120
+ const normalizedAdditionalStackFileNames = Array.isArray(selectedAdditionalStackFileNames)
121
+ ? Array.from(new Set(selectedAdditionalStackFileNames.filter((stackFileName) => stackFileName && stackFileName !== selectedStackFileName)))
122
+ : [];
123
+ const normalizedAdditionalBlueprintFileNames = Array.isArray(selectedAdditionalBlueprintFileNames)
124
+ ? Array.from(new Set(selectedAdditionalBlueprintFileNames.filter(
125
+ (blueprintFileName) => blueprintFileName && blueprintFileName !== selectedBlueprintFileName
126
+ )))
127
+ : [];
128
+ const selectedSkillDomainNames = inferSkillDomainNamesFromSelection(
129
+ selectedStackFileName,
130
+ selectedBlueprintFileName,
131
+ normalizedAdditionalStackFileNames,
132
+ normalizedAdditionalBlueprintFileNames
133
+ );
106
134
 
107
135
  const universalRuleFileNames = await collectFileNames(selectedRulesDirectoryPath);
108
136
  const contextBlocks = [];
@@ -166,6 +194,16 @@ export async function buildCompiledRulesContent({
166
194
  ].join('\n')
167
195
  );
168
196
 
197
+ if (normalizedAdditionalStackFileNames.length > 0) {
198
+ contextBlocks.push(
199
+ [
200
+ '## LAYER 2B: ADDITIONAL STACK PROFILES',
201
+ 'This project uses multiple stacks. Load all additional stack profiles below:',
202
+ ...normalizedAdditionalStackFileNames.map((stackFileName, stackIndex) => `${stackIndex + 1}. .agent-context/stacks/${stackFileName}`),
203
+ ].join('\n')
204
+ );
205
+ }
206
+
169
207
  const blueprintFilePath = path.join(selectedBlueprintsDirectoryPath, selectedBlueprintFileName);
170
208
  const blueprintContent = await fs.readFile(blueprintFilePath, 'utf8');
171
209
  const blueprintSummary = firstMarkdownHeading(blueprintContent, selectedBlueprintFileName);
@@ -178,6 +216,18 @@ export async function buildCompiledRulesContent({
178
216
  ].join('\n')
179
217
  );
180
218
 
219
+ if (normalizedAdditionalBlueprintFileNames.length > 0) {
220
+ contextBlocks.push(
221
+ [
222
+ '## LAYER 3A: ADDITIONAL BLUEPRINT PROFILES',
223
+ 'This project uses multiple architecture blueprints. Load all additional blueprint profiles below:',
224
+ ...normalizedAdditionalBlueprintFileNames.map(
225
+ (blueprintFileName, blueprintIndex) => `${blueprintIndex + 1}. .agent-context/blueprints/${blueprintFileName}`
226
+ ),
227
+ ].join('\n')
228
+ );
229
+ }
230
+
181
231
  if (includeCiGuardrails) {
182
232
  contextBlocks.push(
183
233
  [
@@ -300,7 +350,9 @@ export async function compileDynamicContext({
300
350
  targetDirectoryPath,
301
351
  selectedProfileName,
302
352
  selectedStackFileName,
353
+ selectedAdditionalStackFileNames = [],
303
354
  selectedBlueprintFileName,
355
+ selectedAdditionalBlueprintFileNames = [],
304
356
  includeCiGuardrails,
305
357
  }) {
306
358
  const resolvedTargetDirectoryPath = path.resolve(targetDirectoryPath);
@@ -308,7 +360,9 @@ export async function compileDynamicContext({
308
360
  targetDirectoryPath: resolvedTargetDirectoryPath,
309
361
  selectedProfileName,
310
362
  selectedStackFileName,
363
+ selectedAdditionalStackFileNames,
311
364
  selectedBlueprintFileName,
365
+ selectedAdditionalBlueprintFileNames,
312
366
  includeCiGuardrails,
313
367
  });
314
368
 
@@ -160,6 +160,89 @@ export const PROFILE_PRESETS = {
160
160
  },
161
161
  };
162
162
 
163
+ export const PROJECT_SCOPE_CHOICES = [
164
+ {
165
+ key: 'api-service',
166
+ label: 'API service',
167
+ },
168
+ {
169
+ key: 'web-application',
170
+ label: 'Web application',
171
+ },
172
+ {
173
+ key: 'mobile-app',
174
+ label: 'Mobile app',
175
+ },
176
+ {
177
+ key: 'cli-tool',
178
+ label: 'CLI tool',
179
+ },
180
+ {
181
+ key: 'library-sdk',
182
+ label: 'Library / SDK',
183
+ },
184
+ {
185
+ key: 'other',
186
+ label: 'Other',
187
+ },
188
+ ];
189
+
190
+ export const PROJECT_SCOPE_STACK_FILTERS = {
191
+ 'api-service': ['typescript.md', 'python.md', 'go.md', 'java.md', 'php.md', 'csharp.md', 'ruby.md', 'rust.md'],
192
+ 'web-application': ['typescript.md', 'python.md', 'go.md', 'java.md', 'php.md', 'csharp.md', 'ruby.md', 'rust.md'],
193
+ 'mobile-app': ['react-native.md', 'flutter.md'],
194
+ 'cli-tool': ['typescript.md', 'python.md', 'go.md', 'rust.md', 'ruby.md', 'java.md', 'csharp.md'],
195
+ 'library-sdk': ['typescript.md', 'python.md', 'go.md', 'rust.md', 'java.md', 'csharp.md', 'php.md', 'ruby.md'],
196
+ other: null,
197
+ };
198
+
199
+ export const WEB_FRONTEND_STACK_CANDIDATES = ['typescript.md'];
200
+
201
+ export const WEB_BACKEND_STACK_CANDIDATES = [
202
+ 'typescript.md',
203
+ 'python.md',
204
+ 'go.md',
205
+ 'java.md',
206
+ 'php.md',
207
+ 'csharp.md',
208
+ 'ruby.md',
209
+ 'rust.md',
210
+ ];
211
+
212
+ export const WEB_FRONTEND_BLUEPRINT_CANDIDATES = [
213
+ 'api-nextjs.md',
214
+ ];
215
+
216
+ export const WEB_BACKEND_BLUEPRINT_CANDIDATES = [
217
+ 'nestjs-logic.md',
218
+ 'fastapi-service.md',
219
+ 'go-service.md',
220
+ 'spring-boot-api.md',
221
+ 'laravel-api.md',
222
+ 'aspnet-api.md',
223
+ 'graphql-grpc-api.md',
224
+ 'api-nextjs.md',
225
+ ];
226
+
227
+ export const RUNTIME_ENVIRONMENT_CHOICES = [
228
+ {
229
+ key: 'linux-wsl',
230
+ label: 'Linux / WSL',
231
+ },
232
+ {
233
+ key: 'windows',
234
+ label: 'Windows',
235
+ },
236
+ {
237
+ key: 'linux',
238
+ label: 'Linux',
239
+ },
240
+ {
241
+ key: 'macos',
242
+ label: 'macOS',
243
+ },
244
+ ];
245
+
163
246
  export const entryPointFiles = [
164
247
  '.cursorrules',
165
248
  '.windsurfrules',
@@ -133,6 +133,7 @@ export async function detectProjectContext(targetDirectoryPath) {
133
133
  return {
134
134
  hasExistingProjectFiles,
135
135
  recommendedStackFileName: null,
136
+ secondaryStackFileNames: [],
136
137
  recommendedBlueprintFileName: null,
137
138
  confidenceLabel: null,
138
139
  confidenceScore: 0,
@@ -164,6 +165,10 @@ export async function detectProjectContext(targetDirectoryPath) {
164
165
  confidenceScore: Number(detectionCandidate.confidenceScore.toFixed(2)),
165
166
  evidence: detectionCandidate.evidence,
166
167
  }));
168
+ const secondaryStackFileNames = rankedCandidates
169
+ .slice(1)
170
+ .filter((rankedCandidate) => (strongestCandidate.confidenceScore - rankedCandidate.confidenceScore) < 0.08)
171
+ .map((rankedCandidate) => rankedCandidate.stackFileName);
167
172
  const detectionReasoning = isAmbiguous
168
173
  ? `Top signal ${toTitleCase(strongestCandidate.stackFileName)} is close to ${toTitleCase(secondStrongestCandidate.stackFileName)} (confidence gap ${confidenceGap}).`
169
174
  : `Top signal ${toTitleCase(strongestCandidate.stackFileName)} won with confidence ${strongestCandidate.confidenceScore.toFixed(2)} from markers: ${strongestCandidate.evidence.join(', ') || 'none'}.`;
@@ -171,6 +176,7 @@ export async function detectProjectContext(targetDirectoryPath) {
171
176
  return {
172
177
  hasExistingProjectFiles,
173
178
  recommendedStackFileName: strongestCandidate.stackFileName,
179
+ secondaryStackFileNames,
174
180
  recommendedBlueprintFileName: BLUEPRINT_RECOMMENDATIONS[strongestCandidate.stackFileName] || null,
175
181
  confidenceLabel,
176
182
  confidenceScore: strongestCandidate.confidenceScore,
@@ -194,7 +200,11 @@ export function buildDetectionSummary(projectDetection) {
194
200
  ? ` Confidence gap: ${projectDetection.confidenceGap}.`
195
201
  : '';
196
202
 
197
- return `This folder looks like ${toTitleCase(projectDetection.recommendedStackFileName)} with ${projectDetection.confidenceLabel} confidence based on ${readableEvidence}.${confidenceGapSummary}`;
203
+ const secondaryStacksSummary = projectDetection.secondaryStackFileNames?.length
204
+ ? ` Secondary stack signals: ${projectDetection.secondaryStackFileNames.map((stackFileName) => toTitleCase(stackFileName)).join(', ')}.`
205
+ : '';
206
+
207
+ return `This folder looks like ${toTitleCase(projectDetection.recommendedStackFileName)} with ${projectDetection.confidenceLabel} confidence based on ${readableEvidence}.${confidenceGapSummary}${secondaryStacksSummary}`;
198
208
  }
199
209
 
200
210
  export function formatDetectionCandidates(rankedCandidates) {
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Cross-agent memory continuity utilities.
3
+ * Provides provider-agnostic observation normalization, privacy redaction,
4
+ * lightweight indexing, and selective hydration helpers.
5
+ */
6
+
7
+ const PRIVATE_BLOCK_PATTERN = /<private>[\s\S]*?<\/private>/gi;
8
+
9
+ const INLINE_SENSITIVE_PATTERNS = [
10
+ {
11
+ reason: 'api-key-like-value',
12
+ pattern: /\b(api[_-]?key)\b\s*[:=]\s*[^\s,;]+/gi,
13
+ replacer: (_match, fieldName) => `${fieldName}=[REDACTED]`,
14
+ },
15
+ {
16
+ reason: 'token-like-value',
17
+ pattern: /\b(token)\b\s*[:=]\s*[^\s,;]+/gi,
18
+ replacer: (_match, fieldName) => `${fieldName}=[REDACTED]`,
19
+ },
20
+ {
21
+ reason: 'password-like-value',
22
+ pattern: /\b(password|passwd|pwd)\b\s*[:=]\s*[^\s,;]+/gi,
23
+ replacer: (_match, fieldName) => `${fieldName}=[REDACTED]`,
24
+ },
25
+ {
26
+ reason: 'bearer-token',
27
+ pattern: /\bBearer\s+[A-Za-z0-9._-]+/g,
28
+ replacer: () => 'Bearer [REDACTED]',
29
+ },
30
+ ];
31
+
32
+ export const MEMORY_SCHEMA_VERSION = '1.0.0';
33
+
34
+ export const SUPPORTED_MEMORY_ADAPTER_IDS = Object.freeze([
35
+ 'claude-code',
36
+ 'gemini-cli',
37
+ 'vscode-chat',
38
+ ]);
39
+
40
+ export const SUPPORTED_MEMORY_EVENT_TYPES = Object.freeze([
41
+ 'prompt',
42
+ 'tool-use',
43
+ 'decision',
44
+ 'summary',
45
+ 'issue',
46
+ 'context',
47
+ ]);
48
+
49
+ function toIsoTimestamp(rawValue) {
50
+ if (typeof rawValue !== 'string' || rawValue.trim().length === 0) {
51
+ return new Date().toISOString();
52
+ }
53
+
54
+ const parsedDate = new Date(rawValue);
55
+ if (Number.isNaN(parsedDate.getTime())) {
56
+ return new Date().toISOString();
57
+ }
58
+
59
+ return parsedDate.toISOString();
60
+ }
61
+
62
+ function toNonEmptyString(rawValue, fallbackValue = '') {
63
+ if (typeof rawValue !== 'string') {
64
+ return fallbackValue;
65
+ }
66
+
67
+ const normalizedValue = rawValue.trim();
68
+ return normalizedValue.length > 0 ? normalizedValue : fallbackValue;
69
+ }
70
+
71
+ function normalizeTags(rawTags) {
72
+ if (!Array.isArray(rawTags)) {
73
+ return [];
74
+ }
75
+
76
+ const tagSet = new Set();
77
+ for (const rawTag of rawTags) {
78
+ const normalizedTag = toNonEmptyString(String(rawTag || '')).toLowerCase();
79
+ if (normalizedTag) {
80
+ tagSet.add(normalizedTag);
81
+ }
82
+ }
83
+
84
+ return Array.from(tagSet);
85
+ }
86
+
87
+ export function estimateTokenUsage(rawText = '') {
88
+ const normalizedText = String(rawText || '');
89
+ return Math.max(1, Math.ceil(normalizedText.length / 4));
90
+ }
91
+
92
+ export function redactSensitiveMemoryText(rawText = '') {
93
+ let normalizedText = String(rawText || '');
94
+ const redactionReasons = new Set();
95
+ let privateTagRedactionCount = 0;
96
+ let inlineRedactionCount = 0;
97
+
98
+ normalizedText = normalizedText.replace(PRIVATE_BLOCK_PATTERN, () => {
99
+ privateTagRedactionCount += 1;
100
+ redactionReasons.add('private-tag');
101
+ return '[REDACTED_PRIVATE_BLOCK]';
102
+ });
103
+
104
+ for (const sensitivePattern of INLINE_SENSITIVE_PATTERNS) {
105
+ normalizedText = normalizedText.replace(sensitivePattern.pattern, (...replacerArguments) => {
106
+ inlineRedactionCount += 1;
107
+ redactionReasons.add(sensitivePattern.reason);
108
+ return sensitivePattern.replacer(...replacerArguments);
109
+ });
110
+ }
111
+
112
+ return {
113
+ redactedText: normalizedText,
114
+ wasRedacted: privateTagRedactionCount > 0 || inlineRedactionCount > 0,
115
+ privateTagRedactionCount,
116
+ inlineRedactionCount,
117
+ redactionReasons: Array.from(redactionReasons),
118
+ };
119
+ }
120
+
121
+ export function normalizeMemoryObservation(rawObservation, options = {}) {
122
+ const fallbackAdapterId = options.fallbackAdapterId || 'unknown-adapter';
123
+ const observationId = toNonEmptyString(rawObservation?.id, `${fallbackAdapterId}-${Date.now()}`);
124
+ const adapterId = toNonEmptyString(rawObservation?.adapterId, fallbackAdapterId);
125
+
126
+ const eventTypeCandidate = toNonEmptyString(rawObservation?.eventType, 'context').toLowerCase();
127
+ const eventType = SUPPORTED_MEMORY_EVENT_TYPES.includes(eventTypeCandidate)
128
+ ? eventTypeCandidate
129
+ : 'context';
130
+
131
+ const rawDetail = toNonEmptyString(rawObservation?.detail, '');
132
+ const detailRedaction = redactSensitiveMemoryText(rawDetail);
133
+
134
+ const rawSummary = toNonEmptyString(rawObservation?.summary, detailRedaction.redactedText.slice(0, 220));
135
+ const summaryRedaction = redactSensitiveMemoryText(rawSummary);
136
+
137
+ const title = toNonEmptyString(rawObservation?.title, `${eventType} from ${adapterId}`);
138
+ const tags = normalizeTags(rawObservation?.tags);
139
+
140
+ return {
141
+ id: observationId,
142
+ projectId: toNonEmptyString(rawObservation?.projectId, 'default-project'),
143
+ sessionId: toNonEmptyString(rawObservation?.sessionId, 'default-session'),
144
+ adapterId,
145
+ eventType,
146
+ timestamp: toIsoTimestamp(rawObservation?.timestamp),
147
+ title,
148
+ summary: summaryRedaction.redactedText,
149
+ detail: detailRedaction.redactedText,
150
+ tags,
151
+ privacy: {
152
+ level: toNonEmptyString(rawObservation?.privacyLevel, 'internal'),
153
+ redactionApplied: detailRedaction.wasRedacted || summaryRedaction.wasRedacted,
154
+ redactionReasons: Array.from(new Set([
155
+ ...detailRedaction.redactionReasons,
156
+ ...summaryRedaction.redactionReasons,
157
+ ])),
158
+ privateTagRedactionCount: detailRedaction.privateTagRedactionCount + summaryRedaction.privateTagRedactionCount,
159
+ inlineRedactionCount: detailRedaction.inlineRedactionCount + summaryRedaction.inlineRedactionCount,
160
+ },
161
+ };
162
+ }
163
+
164
+ export function scoreObservationRelevance(queryText, normalizedObservation) {
165
+ const normalizedQuery = toNonEmptyString(queryText, '').toLowerCase();
166
+ if (!normalizedQuery) {
167
+ return 0;
168
+ }
169
+
170
+ const queryTerms = normalizedQuery
171
+ .split(/\s+/)
172
+ .map((queryTerm) => queryTerm.trim())
173
+ .filter((queryTerm) => queryTerm.length > 2);
174
+
175
+ if (queryTerms.length === 0) {
176
+ return 0;
177
+ }
178
+
179
+ const searchableContent = [
180
+ normalizedObservation.title,
181
+ normalizedObservation.summary,
182
+ normalizedObservation.detail,
183
+ normalizedObservation.tags.join(' '),
184
+ normalizedObservation.eventType,
185
+ normalizedObservation.adapterId,
186
+ ].join(' ').toLowerCase();
187
+
188
+ let matchCount = 0;
189
+ for (const queryTerm of queryTerms) {
190
+ if (searchableContent.includes(queryTerm)) {
191
+ matchCount += 1;
192
+ }
193
+ }
194
+
195
+ return Number((matchCount / queryTerms.length).toFixed(4));
196
+ }
197
+
198
+ export function buildSessionStartIndex(normalizedObservations, options = {}) {
199
+ const queryText = toNonEmptyString(options.queryText, '');
200
+ const maxIndexEntries = Number.isFinite(Number(options.limit)) ? Math.max(1, Number(options.limit)) : 8;
201
+
202
+ const rankedEntries = normalizedObservations
203
+ .map((normalizedObservation) => {
204
+ const relevanceScore = scoreObservationRelevance(queryText, normalizedObservation);
205
+ const indexLine = `${normalizedObservation.id}|${normalizedObservation.adapterId}|${normalizedObservation.eventType}|${normalizedObservation.title}`;
206
+ const indexTokenEstimate = estimateTokenUsage(indexLine);
207
+
208
+ return {
209
+ id: normalizedObservation.id,
210
+ adapterId: normalizedObservation.adapterId,
211
+ eventType: normalizedObservation.eventType,
212
+ timestamp: normalizedObservation.timestamp,
213
+ title: normalizedObservation.title,
214
+ summarySnippet: normalizedObservation.summary.slice(0, 120),
215
+ tags: normalizedObservation.tags,
216
+ relevanceScore,
217
+ indexTokenEstimate,
218
+ };
219
+ })
220
+ .sort((leftEntry, rightEntry) => {
221
+ if (rightEntry.relevanceScore !== leftEntry.relevanceScore) {
222
+ return rightEntry.relevanceScore - leftEntry.relevanceScore;
223
+ }
224
+
225
+ return rightEntry.timestamp.localeCompare(leftEntry.timestamp);
226
+ });
227
+
228
+ const indexEntries = rankedEntries.slice(0, maxIndexEntries);
229
+ const totalTokenEstimate = indexEntries.reduce(
230
+ (tokenAccumulator, indexEntry) => tokenAccumulator + indexEntry.indexTokenEstimate,
231
+ 0
232
+ );
233
+
234
+ return {
235
+ indexEntries,
236
+ totalTokenEstimate,
237
+ totalCandidateCount: rankedEntries.length,
238
+ };
239
+ }
240
+
241
+ export function hydrateIndexedObservations(indexEntries, normalizedObservations, options = {}) {
242
+ const fullFetchLimit = Number.isFinite(Number(options.fullFetchLimit))
243
+ ? Math.max(1, Number(options.fullFetchLimit))
244
+ : 2;
245
+
246
+ const observationLookup = new Map(normalizedObservations.map((normalizedObservation) => [
247
+ normalizedObservation.id,
248
+ normalizedObservation,
249
+ ]));
250
+
251
+ const selectedIds = indexEntries.slice(0, fullFetchLimit).map((indexEntry) => indexEntry.id);
252
+ const hydratedObservations = selectedIds
253
+ .map((selectedId) => observationLookup.get(selectedId))
254
+ .filter(Boolean);
255
+
256
+ const hydrationTokenEstimate = hydratedObservations.reduce(
257
+ (tokenAccumulator, hydratedObservation) => tokenAccumulator + estimateTokenUsage(hydratedObservation.detail),
258
+ 0
259
+ );
260
+
261
+ return {
262
+ selectedIds,
263
+ hydratedObservations,
264
+ hydrationTokenEstimate,
265
+ };
266
+ }