File size: 3,274 Bytes
7a1ad33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import * as fsSync from 'node:fs';
import * as fs from 'node:fs/promises';
import type { ContextManagementConfig } from './types.js';
import {
  generalistProfile,
  stressTestProfile,
  powerUserProfile,
  type ContextProfile,
} from './profiles.js';
import { SchemaValidator } from '../../utils/schemaValidator.js';
import { getContextManagementConfigSchema } from './schema.js';
import type { ContextProcessorRegistry } from './registry.js';
import { getErrorMessage } from '../../utils/errors.js';

/**
 * Loads and validates a sidecar config from a specific file path.
 * Throws an error if the file cannot be read, parsed, or fails schema validation.
 */
async function loadConfigFromFile(
  sidecarPath: string,
  registry: ContextProcessorRegistry,
): Promise<ContextProfile> {
  const fileContent = await fs.readFile(sidecarPath, 'utf8');
  let parsed: unknown;
  try {
    parsed = JSON.parse(fileContent);
  } catch (error) {
    throw new Error(
      `Failed to parse Sidecar configuration file at ${sidecarPath}: ${getErrorMessage(
        error,
      )}`,
    );
  }

  // Validate the complete structure, including deep options
  const validationError = SchemaValidator.validate(
    getContextManagementConfigSchema(registry),
    parsed,
  );

  if (validationError) {
    throw new Error(
      `Invalid sidecar configuration in ${sidecarPath}. Validation error: ${validationError}`,
    );
  }

  // Extract strictly what we need.
  // Why this unsafe cast is acceptable:
  // SchemaValidator just ran \`getSidecarConfigSchema(registry)\` against \`parsed\`.
  // That function dynamically maps the \`processorOptions\` to strict JSON schema definitions,
  // so we know with absolute certainty at runtime that \`parsed\` conforms to this shape.
  // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
  const validConfig = parsed as ContextManagementConfig;
  return {
    ...generalistProfile,
    config: {
      ...generalistProfile.config,
      ...(validConfig.budget ? { budget: validConfig.budget } : {}),
      ...(validConfig.processorOptions
        ? { processorOptions: validConfig.processorOptions }
        : {}),
    },
  };
}

/**
 * Generates a Sidecar JSON graph from the experimental config file path or defaults.
 * If a config file is present but invalid, this will THROW to prevent silent misconfiguration.
 */
export async function loadContextManagementConfig(
  sidecarPath: string | undefined,
  registry: ContextProcessorRegistry,
): Promise<ContextProfile> {
  if (sidecarPath === 'stressTestProfile') {
    return stressTestProfile;
  }

  if (sidecarPath === 'powerUserProfile') {
    return powerUserProfile;
  }

  if (sidecarPath === 'generalistProfile') {
    return generalistProfile;
  }

  if (sidecarPath && fsSync.existsSync(sidecarPath)) {
    const size = fsSync.statSync(sidecarPath).size;
    // If the file exists but is completely empty (0 bytes), it's safe to fallback.
    if (size === 0) {
      return generalistProfile;
    }

    // If the file has content, enforce strict validation and throw on failure.
    return loadConfigFromFile(sidecarPath, registry);
  }

  return generalistProfile;
}