@gitsense/gsc-utils 0.2.4 → 0.2.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitsense/gsc-utils",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Utilities for GitSense Chat (GSC)",
5
5
  "main": "dist/gsc-utils.cjs.js",
6
6
  "module": "dist/gsc-utils.esm.js",
@@ -0,0 +1,139 @@
1
+ /*
2
+ * Component: AnalyzerUtils Discovery
3
+ * Block-UUID: 0b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5f
4
+ * Parent-UUID: N/A
5
+ * Version: 1.0.0
6
+ * Description: Provides utility functions for discovering available analyzers.
7
+ * Language: JavaScript
8
+ * Created-at: 2025-08-28T23:48:00.000Z
9
+ * Authors: Gemini 2.5 Flash (v1.0.0)
10
+ */
11
+
12
+
13
+ const fs = require('fs').promises;
14
+ const path = require('path');
15
+
16
+ /**
17
+ * Reads and parses the config.json file in a directory.
18
+ * @param {string} dirPath - The path to the directory.
19
+ * @returns {Promise<object|null>} A promise that resolves to the parsed config object
20
+ * or null if the file doesn't exist or is invalid.
21
+ */
22
+ async function readConfig(dirPath) {
23
+ const configPath = path.join(dirPath, 'config.json');
24
+ try {
25
+ const fileContent = await fs.readFile(configPath, 'utf8');
26
+ return JSON.parse(fileContent);
27
+ } catch (error) {
28
+ if (error.code !== 'ENOENT') {
29
+ // Log a warning if config.json exists but is malformed
30
+ console.warn(`Warning: Failed to parse config.json in ${dirPath}: ${error.message}`);
31
+ }
32
+ return null; // Return null if file not found or parsing failed
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Checks if a directory name is valid based on the rules in messages/analyze/README.md.
38
+ * Allowed: a-z, A-Z, 0-9, dash (-), underscore (_). Cannot start with underscore or contain dots.
39
+ * @param {string} name - The directory name to check.
40
+ * @returns {boolean} True if the name is valid, false otherwise.
41
+ */
42
+ function isValidDirName(name) {
43
+ // Exclude names starting with underscore or containing dots
44
+ if (name.startsWith('_') || name.includes('.')) {
45
+ return false;
46
+ }
47
+ // Check for allowed characters
48
+ return /^[a-zA-Z0-9_-]+$/.test(name);
49
+ }
50
+
51
+ /**
52
+ * Discovers and lists all available analyzers by traversing the directory structure.
53
+ * An analyzer is considered valid if a '1.md' file exists in the instructions directory.
54
+ *
55
+ * @param {string} analyzeMessagesBasePath - The absolute or relative path to the base directory containing the analyzer message files (e.g., 'messages/analyze').
56
+ * @returns {Promise<Array<{id: string, label: string}>>} A promise that resolves to an array of analyzer objects.
57
+ */
58
+ async function getAnalyzers(analyzeMessagesBasePath) {
59
+ const analyzers = [];
60
+
61
+ try {
62
+ const analyzerEntries = await fs.readdir(analyzeMessagesBasePath, { withFileTypes: true });
63
+
64
+ for (const analyzerEntry of analyzerEntries) {
65
+ if (analyzerEntry.isDirectory() && isValidDirName(analyzerEntry.name)) {
66
+ const analyzerName = analyzerEntry.name;
67
+ const analyzerPath = path.join(analyzeMessagesBasePath, analyzerName);
68
+ const analyzerConfig = await readConfig(analyzerPath);
69
+ const analyzerLabel = analyzerConfig?.label || analyzerName;
70
+
71
+ const contentEntries = await fs.readdir(analyzerPath, { withFileTypes: true });
72
+
73
+ for (const contentEntry of contentEntries) {
74
+ if (contentEntry.isDirectory() && isValidDirName(contentEntry.name)) {
75
+ const contentType = contentEntry.name;
76
+ const contentPath = path.join(analyzerPath, contentType);
77
+ const contentConfig = await readConfig(contentPath);
78
+ const contentLabel = contentConfig?.label || contentType;
79
+
80
+ const instructionsEntries = await fs.readdir(contentPath, { withFileTypes: true });
81
+
82
+ for (const instructionsEntry of instructionsEntries) {
83
+ if (instructionsEntry.isDirectory() && isValidDirName(instructionsEntry.name)) {
84
+ const instructionsType = instructionsEntry.name;
85
+ const instructionsPath = path.join(contentPath, instructionsType);
86
+ const instructionsConfig = await readConfig(instructionsPath);
87
+ const instructionsLabel = instructionsConfig?.label || instructionsType;
88
+
89
+ // Check for the existence of 1.md to confirm a valid analyzer configuration
90
+ const instructionsFilePath = path.join(instructionsPath, '1.md');
91
+ try {
92
+ await fs.access(instructionsFilePath); // Check if file exists and is accessible
93
+
94
+ // If analyzerName starts with 'tutorial-', check its last modified time.
95
+ if (analyzerName.startsWith('tutorial-')) {
96
+ const stats = await fs.stat(instructionsFilePath);
97
+ const lastModified = stats.mtime.getTime(); // Get timestamp in milliseconds
98
+ const sixtyMinutesAgo = Date.now() - (60 * 60 * 1000); // Current time - 60 minutes in ms
99
+
100
+ if (lastModified < sixtyMinutesAgo) {
101
+ // This tutorial analyzer has expired, skip it.
102
+ continue;
103
+ }
104
+ }
105
+ // Construct the analyzer ID and label
106
+ const analyzerId = `${analyzerName}::${contentType}::${instructionsType}`;
107
+ const analyzerFullLabel = `${analyzerLabel} (${contentLabel} - ${instructionsLabel})`;
108
+
109
+ analyzers.push({
110
+ id: analyzerId,
111
+ label: analyzerFullLabel,
112
+ protected: analyzerConfig?.protected || false
113
+ });
114
+ } catch (error) {
115
+ // If 1.md doesn't exist, this is not a complete analyzer configuration, skip.
116
+ if (error.code !== 'ENOENT') {
117
+ console.warn(`Warning: Error accessing 1.md for ${analyzerId}: ${error.message}`);
118
+ }
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ } catch (error) {
127
+ console.error(`Error traversing analyze messages directory ${analyzeMessagesBasePath}: ${error.message}`);
128
+ // Depending on requirements, you might want to throw the error or return an empty array
129
+ throw error; // Re-throw to indicate failure
130
+ }
131
+
132
+ return analyzers;
133
+ }
134
+
135
+ module.exports = {
136
+ getAnalyzers,
137
+ readConfig,
138
+ isValidDirName,
139
+ };
@@ -2,20 +2,30 @@
2
2
  * Component: AnalyzerUtils Index
3
3
  * Block-UUID: b403b6a1-230b-4247-8cd6-2a3d068f4bbf
4
4
  * Parent-UUID: N/A
5
- * Version: 1.0.0
5
+ * Version: 1.1.0
6
6
  * Description: Aggregates and exports all utility functions from the AnalyzerUtils module.
7
7
  * Language: JavaScript
8
8
  * Created-at: 2025-08-28T15:56:40.319Z
9
- * Authors: Gemini 2.5 Flash (v1.0.0)
9
+ * Authors: Gemini 2.5 Flash (v1.0.0), Gemini 2.5 Flash (v1.1.0)
10
10
  */
11
11
 
12
12
 
13
13
  const { buildChatIdToPathMap } = require('./contextMapper');
14
14
  const { processLLMAnalysisResponse } = require('./responseProcessor');
15
15
  const { validateLLMAnalysisData } = require('./dataValidator');
16
+ const { getAnalyzers } = require('./discovery');
17
+ const { saveConfiguration } = require('./saver');
18
+ const { getAnalyzerSchema } = require('./schemaLoader');
19
+ const { deleteAnalyzer } = require('./management');
20
+ const { getAnalyzerInstructionsContent } = require('./instructionLoader');
16
21
 
17
22
  module.exports = {
18
23
  buildChatIdToPathMap,
19
24
  processLLMAnalysisResponse,
20
- validateLLMAnalysisData
25
+ validateLLMAnalysisData,
26
+ getAnalyzers,
27
+ getAnalyzerSchema,
28
+ deleteAnalyzer,
29
+ getAnalyzerInstructionsContent,
30
+ saveConfiguration,
21
31
  };
@@ -0,0 +1,58 @@
1
+ /*
2
+ * Component: AnalyzerUtils Instruction Loader
3
+ * Block-UUID: 0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5e
4
+ * Parent-UUID: N/A
5
+ * Version: 1.0.0
6
+ * Description: Provides utility functions for loading raw analyzer instruction content.
7
+ * Language: JavaScript
8
+ * Created-at: 2025-08-28T23:48:00.000Z
9
+ * Authors: Gemini 2.5 Flash (v1.0.0)
10
+ */
11
+
12
+
13
+ const fs = require('fs').promises;
14
+ const path = require('path');
15
+
16
+ /**
17
+ * Retrieves the raw Markdown content of the analyzer's '1.md' instruction file.
18
+ *
19
+ * @param {string} analyzeMessagesBasePath - The absolute path to the base directory containing the analyzer message files (e.g., 'messages/analyze').
20
+ * @param {string} analyzerId - The unique ID of the analyzer (format: 'analyzer_name::content_type::instructions_type').
21
+ * @returns {Promise<string|null>} A promise that resolves with the full Markdown content of the '1.md' file, or null if not found/invalid.
22
+ */
23
+ async function getAnalyzerInstructionsContent(analyzeMessagesBasePath, analyzerId) {
24
+ if (typeof analyzeMessagesBasePath !== 'string' || analyzeMessagesBasePath.trim() === '') {
25
+ console.error('Error: analyzeMessagesBasePath is required.');
26
+ return null;
27
+ }
28
+ if (typeof analyzerId !== 'string' || analyzerId.trim() === '') {
29
+ console.error('Error: analyzerId is required.');
30
+ return null;
31
+ }
32
+
33
+ const parts = analyzerId.split('::');
34
+ if (parts.length !== 3) {
35
+ console.error(`Error: Invalid analyzerId format. Expected 'analyzer_name::content_type::instructions_type', but got '${analyzerId}'.`);
36
+ return null;
37
+ }
38
+ const [analyzerName, contentType, instructionsType] = parts;
39
+
40
+ const instructionsFilePath = path.join(analyzeMessagesBasePath, analyzerName, contentType, instructionsType, '1.md');
41
+
42
+ try {
43
+ const fileContent = await fs.readFile(instructionsFilePath, 'utf8');
44
+ return fileContent;
45
+ } catch (error) {
46
+ if (error.code === 'ENOENT') {
47
+ console.warn(`Analyzer instructions file not found: ${instructionsFilePath}`);
48
+ return null;
49
+ } else {
50
+ console.error(`Error reading analyzer instructions file ${instructionsFilePath}: ${error.message}`);
51
+ throw error;
52
+ }
53
+ }
54
+ }
55
+
56
+ module.exports = {
57
+ getAnalyzerInstructionsContent
58
+ };
@@ -0,0 +1,131 @@
1
+ /*
2
+ * Component: AnalyzerUtils Management
3
+ * Block-UUID: 0d1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a
4
+ * Parent-UUID: N/A
5
+ * Version: 1.0.0
6
+ * Description: Provides utility functions for managing (deleting) analyzer configurations.
7
+ * Language: JavaScript
8
+ * Created-at: 2025-08-28T23:48:00.000Z
9
+ * Authors: Gemini 2.5 Flash (v1.0.0)
10
+ */
11
+
12
+
13
+ const fs = require('fs').promises;
14
+ const path = require('path');
15
+ const { readConfig } = require('./discovery'); // Import helper from discovery
16
+
17
+ /**
18
+ * Checks if a directory is empty or only contains a config.json.
19
+ * @param {string} dirPath - The path to the directory.
20
+ * @returns {Promise<boolean>} True if the directory is empty or only contains config.json, false otherwise.
21
+ */
22
+ async function isDirectoryEmpty(dirPath) {
23
+ try {
24
+ const files = await fs.readdir(dirPath);
25
+ return files.length === 0 || (files.length === 1 && files[0] === 'config.json');
26
+ } catch (error) {
27
+ if (error.code === 'ENOENT') {
28
+ return true; // Directory doesn't exist, so it's "empty" for our purpose
29
+ }
30
+ throw error; // Re-throw other errors
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Deletes a specific analyzer configuration and intelligently cleans up empty directories.
36
+ *
37
+ * @param {string} analyzeMessagesBasePath - The absolute or relative path to the base directory containing the analyzer message files (e.g., 'messages/analyze').
38
+ * @param {string} analyzerId - The unique ID of the analyzer to delete (format: 'analyzer_name::content_type::instructions_type').
39
+ * @returns {Promise<{success: boolean, message: string}>} A promise that resolves with a result object indicating success or failure.
40
+ */
41
+ async function deleteAnalyzer(analyzeMessagesBasePath, analyzerId) {
42
+ if (typeof analyzeMessagesBasePath !== 'string' || analyzeMessagesBasePath.trim() === '') {
43
+ return { success: false, message: 'analyzeMessagesBasePath is required.' };
44
+ }
45
+ if (typeof analyzerId !== 'string' || analyzerId.trim() === '') {
46
+ return { success: false, message: 'analyzerId is required.' };
47
+ }
48
+
49
+ const parts = analyzerId.split('::');
50
+ if (parts.length !== 3) {
51
+ return { success: false, message: `Invalid analyzerId format. Expected 'analyzer_name::content_type::instructions_type', but got '${analyzerId}'.` };
52
+ }
53
+ const [analyzerName, contentType, instructionsType] = parts;
54
+
55
+ const analyzerDir = path.join(analyzeMessagesBasePath, analyzerName);
56
+ const contentDir = path.join(analyzerDir, contentType);
57
+ const instructionsDir = path.join(contentDir, instructionsType);
58
+ const instructionsFilePath = path.join(instructionsDir, '1.md');
59
+
60
+ try {
61
+ // 1. Check for protection at all levels
62
+ const analyzerConfig = await readConfig(analyzerDir);
63
+ if (analyzerConfig?.protected) {
64
+ return { success: false, message: `Analyzer '${analyzerName}' is protected and cannot be deleted.` };
65
+ }
66
+
67
+ const contentConfig = await readConfig(contentDir);
68
+ if (contentConfig?.protected) {
69
+ return { success: false, message: `Content type '${contentType}' for analyzer '${analyzerName}' is protected and cannot be deleted.` };
70
+ }
71
+
72
+ const instructionsConfig = await readConfig(instructionsDir);
73
+ if (instructionsConfig?.protected) {
74
+ return { success: false, message: `Instructions type '${instructionsType}' for content type '${contentType}' is protected and cannot be deleted.` };
75
+ }
76
+
77
+ // 2. Delete the 1.md file
78
+ try {
79
+ await fs.unlink(instructionsFilePath);
80
+ } catch (error) {
81
+ if (error.code === 'ENOENT') {
82
+ return { success: false, message: `Analyzer instructions file not found: ${instructionsFilePath}. It may have already been deleted.` };
83
+ }
84
+ throw error; // Re-throw other errors
85
+ }
86
+
87
+ // 3. Intelligently delete empty directories, cascading upwards
88
+ let deletedDirs = [];
89
+
90
+ // Check and delete instructions directory
91
+ if (await isDirectoryEmpty(instructionsDir)) {
92
+ try {
93
+ await fs.rmdir(instructionsDir);
94
+ deletedDirs.push(instructionsDir);
95
+ } catch (error) {
96
+ console.warn(`Warning: Could not remove empty instructions directory ${instructionsDir}: ${error.message}`);
97
+ }
98
+ }
99
+
100
+ // Check and delete content directory
101
+ if (await isDirectoryEmpty(contentDir)) {
102
+ try {
103
+ await fs.rmdir(contentDir);
104
+ deletedDirs.push(contentDir);
105
+ } catch (error) {
106
+ console.warn(`Warning: Could not remove empty content directory ${contentDir}: ${error.message}`);
107
+ }
108
+ }
109
+
110
+ // Check and delete analyzer directory
111
+ if (await isDirectoryEmpty(analyzerDir)) {
112
+ try {
113
+ await fs.rmdir(analyzerDir);
114
+ deletedDirs.push(analyzerDir);
115
+ } catch (error) {
116
+ console.warn(`Warning: Could not remove empty analyzer directory ${analyzerDir}: ${error.message}`);
117
+ }
118
+ }
119
+
120
+ return { success: true, message: `Analyzer '${analyzerId}' deleted successfully. Cleaned up directories: ${deletedDirs.join(', ') || 'None'}.` };
121
+
122
+ } catch (error) {
123
+ console.error(`Error deleting analyzer '${analyzerId}': ${error.message}`);
124
+ return { success: false, message: `Failed to delete analyzer: ${error.message}` };
125
+ }
126
+ }
127
+
128
+ module.exports = {
129
+ deleteAnalyzer,
130
+ isDirectoryEmpty,
131
+ };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Component: Analyzer Saver Utility
3
+ * Block-UUID: a373f4ba-89ce-465f-8624-24258c923e61
4
+ * Parent-UUID: N/A
5
+ * Version: 1.1.0
6
+ * Description: Utility function to save or update an analyzer configuration based on its ID and content.
7
+ * Language: JavaScript
8
+ * Created-at: 2025-07-12T04:12:33.454Z
9
+ * Authors: Gemini 2.5 Flash Thinking (v1.0.0), Gemini 2.5 Flash Thinking (v1.1.0)
10
+ */
11
+
12
+
13
+ const fs = require('fs').promises;
14
+ const path = require('path');
15
+
16
+ /**
17
+ * Saves or updates an analyzer configuration.
18
+ *
19
+ * This function takes the analyzer ID and its full instructions content,
20
+ * parses the ID to determine the directory structure, creates directories
21
+ * if necessary, saves the instructions to '1.md'. Optionally, it can
22
+ * ensure config.json files exist with labels derived from directory names.
23
+ *
24
+ * @param {string} analyzeMessagesBasePath - The absolute or relative path to the 'messages/analyze' directory.
25
+ * @param {string} analyzerId - The unique ID of the analyzer (format: 'analyzer_name::content_type::instructions_type').
26
+ * @param {string} instructionsContent - The full content of the analyzer instructions message to be saved in '1.md'.
27
+ * @param {object} [options={}] - Optional configuration options.
28
+ * @param {boolean} [options.ensureConfigs=false] - If true, ensures config.json files exist in the analyzer, content, and instructions directories. Defaults to false.
29
+ * @returns {Promise<{success: boolean, message?: string}>} A promise that resolves with a result object.
30
+ */
31
+ async function saveAnalyzerConfiguration(analyzeMessagesBasePath, analyzerId, instructionsContent, options = {}) {
32
+ const { ensureConfigs = false } = options;
33
+
34
+ // 1. Validate inputs
35
+ if (typeof analyzeMessagesBasePath !== 'string' || analyzeMessagesBasePath.trim() === '') {
36
+ return { success: false, message: 'analyzeMessagesBasePath is required.' };
37
+ }
38
+ if (typeof analyzerId !== 'string' || analyzerId.trim() === '') {
39
+ return { success: false, message: 'analyzerId is required.' };
40
+ }
41
+ if (typeof instructionsContent !== 'string' || instructionsContent.trim() === '') {
42
+ return { success: false, message: 'instructionsContent is required.' };
43
+ }
44
+
45
+ // 2. Parse analyzerId
46
+ const parts = analyzerId.split('::');
47
+ if (parts.length !== 3) {
48
+ return { success: false, message: `Invalid analyzerId format. Expected 'analyzer_name::content_type::instructions_type', but got '${analyzerId}'.` };
49
+ }
50
+ const [analyzerName, contentType, instructionsType] = parts;
51
+
52
+ // Helper to validate directory names based on README.md rules
53
+ const isValidDirName = (name) => {
54
+ // Cannot start with underscore, cannot contain dots, must be alphanumeric, dash, or underscore
55
+ return /^[a-zA-Z0-9_-]+$/.test(name) && !name.startsWith('_') && !name.includes('.');
56
+ };
57
+
58
+ if (!isValidDirName(analyzerName)) {
59
+ return { success: false, message: `Invalid analyzer name '${analyzerName}'. Names must be alphanumeric, dash, or underscore, cannot start with underscore, and cannot contain dots.` };
60
+ }
61
+ if (!isValidDirName(contentType)) {
62
+ return { success: false, message: `Invalid content type name '${contentType}'. Names must be alphanumeric, dash, or underscore, cannot start with underscore, and cannot contain dots.` };
63
+ }
64
+ if (!isValidDirName(instructionsType)) {
65
+ return { success: false, message: `Invalid instructions type name '${instructionsType}'. Names must be alphanumeric, dash, or underscore, cannot start with underscore, and cannot contain dots.` };
66
+ }
67
+
68
+ // 3. Construct directory paths
69
+ const analyzerDir = path.join(analyzeMessagesBasePath, analyzerName);
70
+ const contentDir = path.join(analyzerDir, contentType);
71
+ const instructionsDir = path.join(contentDir, instructionsType);
72
+ const instructionsFilePath = path.join(instructionsDir, '1.md');
73
+
74
+ try {
75
+ // 4. Create directories recursively
76
+ await fs.mkdir(instructionsDir, { recursive: true });
77
+
78
+ // 5. Save instructions content to 1.md
79
+ const finalContent = `; role: assistant\n\n\n${instructionsContent}`;
80
+ await fs.writeFile(instructionsFilePath, finalContent, 'utf8');
81
+
82
+ // 6. Optionally create/Update config.json files
83
+ if (ensureConfigs) {
84
+ await ensureConfigJson(analyzerDir, analyzerName);
85
+ await ensureConfigJson(contentDir, contentType);
86
+ await ensureConfigJson(instructionsDir, instructionsType);
87
+ }
88
+
89
+ return { success: true, message: `Analyzer configuration '${analyzerId}' saved successfully.` };
90
+
91
+ } catch (error) {
92
+ console.error(`Error saving analyzer configuration '${analyzerId}':`, error);
93
+ return { success: false, message: `Failed to save analyzer configuration: ${error.message}` };
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Ensures a config.json file exists in the given directory with a label.
99
+ * If the file exists, it reads it and adds the label if missing.
100
+ * If the file doesn't exist, it creates it with the label.
101
+ *
102
+ * @param {string} dirPath - The path to the directory.
103
+ * @param {string} label - The label to ensure is in the config.json.
104
+ */
105
+ async function ensureConfigJson(dirPath, label) {
106
+ const configPath = path.join(dirPath, 'config.json');
107
+ let config = {};
108
+
109
+ try {
110
+ const fileContent = await fs.readFile(configPath, 'utf8');
111
+ config = JSON.parse(fileContent);
112
+ } catch (error) {
113
+ // If file doesn't exist or parsing fails, start with an empty config
114
+ if (error.code !== 'ENOENT') {
115
+ console.warn(`Failed to read or parse existing config.json in ${dirPath}: ${error.message}`);
116
+ }
117
+ config = {}; // Ensure config is an object even on error
118
+ }
119
+
120
+ // Add or update the label if it's missing or empty
121
+ if (!config.label || typeof config.label !== 'string' || config.label.trim() === '') {
122
+ // Capitalize the first letter for the label
123
+ config.label = label.charAt(0).toUpperCase() + label.slice(1);
124
+ }
125
+
126
+ // Write the updated config back to the file
127
+ await fs.writeFile(configPath, JSON.stringify(config, null, 4), 'utf8');
128
+ }
129
+
130
+
131
+ module.exports = {
132
+ saveAnalyzerConfiguration,
133
+ };
@@ -0,0 +1,163 @@
1
+ /*
2
+ * Component: AnalyzerUtils Schema Loader
3
+ * Block-UUID: 0c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f
4
+ * Parent-UUID: N/A
5
+ * Version: 1.0.0
6
+ * Description: Provides utility functions for retrieving and deducing JSON schemas for analyzers.
7
+ * Language: JavaScript
8
+ * Created-at: 2025-08-28T23:48:00.000Z
9
+ * Authors: Gemini 2.5 Flash (v1.0.0)
10
+ */
11
+
12
+
13
+ const fs = require('fs').promises;
14
+ const path = require('path');
15
+ const CodeBlockUtils = require('../CodeBlockUtils');
16
+
17
+ /**
18
+ * Deduces the JSON schema type and format/items from a string value pattern.
19
+ * Handles patterns like "[string: ...]", "[number: ...]", "[datetime: ...]", "[<string>: ...]",
20
+ * and boolean instructions. Defaults to 'string' for unknown patterns.
21
+ *
22
+ * @param {any} value - The value from the raw JSON (expected to be a string pattern).
23
+ * @param {string} fieldName - The name of the field (for logging warnings).
24
+ * @returns {{type: string, format?: string, items?: object}} An object describing the schema type and format/items.
25
+ */
26
+ function deduceSchemaType(value, fieldName) {
27
+ const defaultSchema = { type: 'string' }; // Default fallback
28
+
29
+ if (typeof value !== 'string') {
30
+ const jsType = typeof value;
31
+ if (jsType === 'object' && value !== null) {
32
+ console.warn(`Warning: Unexpected non-string, non-null object/array value for field "${fieldName}". Defaulting to type 'string'. Value:`, value);
33
+ return defaultSchema;
34
+ }
35
+ return { type: jsType };
36
+ }
37
+
38
+ const trimmedValue = value.trim();
39
+
40
+ if (/^\[string:.*\]$/.test(trimmedValue) || (/^\[[^:]+\]$/.test(trimmedValue) && !/^\[(number|datetime|date|<string>):.*\]$/.test(trimmedValue))) {
41
+ return { type: 'string' };
42
+ }
43
+
44
+ if (/^\[number:.*\]$/.test(trimmedValue)) {
45
+ return { type: 'number' };
46
+ }
47
+
48
+ if (/^\[boolean:.*\]$/.test(trimmedValue)) {
49
+ return { type: 'boolean' };
50
+ }
51
+
52
+ if (/^\[date-*time:.*\]$/.test(trimmedValue)) {
53
+ return { type: 'string', format: 'date-time' };
54
+ }
55
+
56
+ if (/^\[date:.*\]$/.test(trimmedValue)) {
57
+ return { type: 'string', format: 'date' };
58
+ }
59
+
60
+ if (/^\[<string>:.*\]$/.test(trimmedValue) || trimmedValue.toLowerCase().includes('array of strings')) {
61
+ return { type: 'array', items: { type: 'string' } };
62
+ }
63
+
64
+ if (trimmedValue.toLowerCase().includes("output 'true' or 'false'") || trimmedValue.toLowerCase().includes("determine if") && (trimmedValue.toLowerCase().includes("true") || trimmedValue.toLowerCase().includes("false"))) {
65
+ return { type: 'boolean' };
66
+ }
67
+
68
+ console.warn(`Warning: Unknown metadata value pattern for field "${fieldName}". Defaulting to type 'string'. Value: "${value}"`);
69
+ return defaultSchema;
70
+ }
71
+
72
+
73
+ /**
74
+ * Retrieves the JSON schema for a specific analyzer.
75
+ * Reads the corresponding '1.md' file, extracts the JSON block,
76
+ * and deduces schema types from the string values.
77
+ *
78
+ * @param {string} analyzeMessagesBasePath - The absolute or relative path to the base directory containing the analyzer message files (e.g., 'messages/analyze').
79
+ * @param {string} analyzerId - The unique ID of the analyzer (format: 'analyzer_name::content_type::instructions_type').
80
+ * @returns {Promise<object|null>} A promise that resolves with the JSON schema object or null if the analyzer ID is invalid or the schema cannot be retrieved/parsed.
81
+ * @throws {Error} If the 1.md file is found but does not contain exactly one JSON code block.
82
+ */
83
+ async function getAnalyzerSchema(analyzeMessagesBasePath, analyzerId) {
84
+ if (typeof analyzeMessagesBasePath !== 'string' || analyzeMessagesBasePath.trim() === '') {
85
+ console.error('Error: analyzeMessagesBasePath is required.');
86
+ return null;
87
+ }
88
+ if (typeof analyzerId !== 'string' || analyzerId.trim() === '') {
89
+ console.error('Error: analyzerId is required.');
90
+ return null;
91
+ }
92
+
93
+ const parts = analyzerId.split('::');
94
+ if (parts.length !== 3) {
95
+ console.error(`Error: Invalid analyzerId format. Expected 'analyzer_name::content_type::instructions_type', but got '${analyzerId}'.`);
96
+ return null;
97
+ }
98
+ const [analyzerName, contentType, instructionsType] = parts;
99
+
100
+ const instructionsFilePath = path.join(analyzeMessagesBasePath, analyzerName, contentType, instructionsType, '1.md');
101
+
102
+ try {
103
+ const fileContent = await fs.readFile(instructionsFilePath, 'utf8');
104
+ const { blocks } = CodeBlockUtils.extractCodeBlocks(fileContent, { silent: true });
105
+ const jsonBlocks = blocks.filter(block => block.type === 'code' && block.language === 'json');
106
+
107
+ if (jsonBlocks.length !== 1) {
108
+ throw new Error(`Expected exactly one JSON code block in ${instructionsFilePath}, but found ${jsonBlocks.length}.`);
109
+ }
110
+
111
+ const jsonBlockContent = jsonBlocks[0].content;
112
+ let rawJson = null;
113
+ try {
114
+ rawJson = JSON.parse(jsonBlockContent);
115
+ } catch (parseError) {
116
+ console.error(`Error parsing JSON content from ${instructionsFilePath}: ${parseError.message}`);
117
+ return null;
118
+ }
119
+
120
+ const schema = {
121
+ type: 'object',
122
+ description: rawJson.description,
123
+ properties: {},
124
+ required: []
125
+ };
126
+
127
+ const metadataProperties = rawJson?.extracted_metadata;
128
+
129
+ if (metadataProperties && typeof metadataProperties === 'object') {
130
+ for (const fieldName in metadataProperties) {
131
+ if (Object.hasOwnProperty.call(metadataProperties, fieldName)) {
132
+ const rawValue = metadataProperties[fieldName];
133
+ const fieldSchema = deduceSchemaType(rawValue, fieldName);
134
+ const description = rawValue.match(/^\[\w+: ([^\]]+)\]/)?.[1] || '';
135
+
136
+ schema.properties[fieldName] = {
137
+ ...fieldSchema,
138
+ description,
139
+ title: fieldName.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
140
+ };
141
+ }
142
+ }
143
+ } else {
144
+ console.warn(`Warning: Could not find 'extracted_metadata' object in JSON block from ${instructionsFilePath}. Schema will be empty.`);
145
+ }
146
+
147
+ return schema;
148
+
149
+ } catch (error) {
150
+ if (error.code === 'ENOENT') {
151
+ console.warn(`Analyzer instructions file not found: ${instructionsFilePath}`);
152
+ return null;
153
+ } else {
154
+ console.error(`Error retrieving or processing schema for analyzer ${analyzerId}: ${error.message}`);
155
+ throw error;
156
+ }
157
+ }
158
+ }
159
+
160
+ module.exports = {
161
+ getAnalyzerSchema,
162
+ deduceSchemaType
163
+ };