spine-framework 0.3.37 → 0.3.39
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/.framework/migrations/003_system_data.sql +16 -16
- package/config/components.json +25 -0
- package/config/deno.lock +108 -0
- package/config/package-lock.json +17183 -0
- package/config/postcss.config.cjs +10 -0
- package/config/tailwind.config.cjs +78 -0
- package/config/tsconfig.cli.json +18 -0
- package/config/tsconfig.json +41 -0
- package/config/tsconfig.node.json +17 -0
- package/config/tsconfig.node.tsbuildinfo +1 -0
- package/config/tsconfig.tsbuildinfo +1 -0
- package/config/vite.config.d.ts +2 -0
- package/config/vite.config.ts +72 -0
- package/netlify.toml +41 -0
- package/package.json +4 -1
- package/scripts/app-install-cli.ts +286 -0
- package/scripts/assemble-frontend.sh +79 -0
- package/scripts/assemble-functions.sh +62 -0
- package/scripts/assemble.sh +35 -0
- package/scripts/boundary-check.sh +106 -0
- package/scripts/build-manifest.sh +80 -0
- package/scripts/check-core-integrity.sh +82 -0
- package/scripts/ingest-chunks.cjs +202 -0
- package/scripts/kb-chunk-parser.cjs +312 -0
- package/scripts/kb-chunk-parser.ts +330 -0
- package/scripts/load-test-app-install.ts +484 -0
- package/scripts/netlify-dev-wrapper.sh +22 -0
- package/scripts/test-login.js +33 -0
- package/scripts/verify-integrity.sh +69 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module scripts/check-core-integrity
|
|
5
|
+
* @audience installer
|
|
6
|
+
* @layer build
|
|
7
|
+
* @stability stable
|
|
8
|
+
*
|
|
9
|
+
* V2 Core Integrity Checker - Verify v2-core hasn't been modified.
|
|
10
|
+
*
|
|
11
|
+
* **Purpose**: Check v2-core directory integrity against stored manifest.
|
|
12
|
+
* - Compares current source and functions hashes with stored values
|
|
13
|
+
* - Used to detect unauthorized modifications to core framework
|
|
14
|
+
*
|
|
15
|
+
* **Process**:
|
|
16
|
+
* 1. Check for manifest file existence
|
|
17
|
+
* 2. Calculate current hashes for src and functions
|
|
18
|
+
* 3. Compare with stored manifest values
|
|
19
|
+
* 4. Report any mismatches
|
|
20
|
+
*
|
|
21
|
+
* **Dependencies**: find, sha256sum, .spine-manifest.json
|
|
22
|
+
*
|
|
23
|
+
* @tags build, integrity, verification, v2-core, security
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
set -euo pipefail
|
|
27
|
+
|
|
28
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
29
|
+
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
30
|
+
CORE_DIR="$PROJECT_ROOT/.framework"
|
|
31
|
+
MANIFEST_FILE="$CORE_DIR/.spine-manifest.json"
|
|
32
|
+
|
|
33
|
+
echo "Checking .framework integrity..."
|
|
34
|
+
|
|
35
|
+
if [ ! -f "$MANIFEST_FILE" ]; then
|
|
36
|
+
echo " ERROR: Manifest file not found at $MANIFEST_FILE"
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
// ─── CHUNK_START: CHECK_CORE_INTEGRITY_MAIN ──────────────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* @chunk-id CHECK_CORE_INTEGRITY_MAIN_1_0_0
|
|
43
|
+
* @version 1.0.0
|
|
44
|
+
* @hash 3799437312b1f95c807f0862818bf01651792c0d5e2bb81d91d734251a76fc75
|
|
45
|
+
* @macro Core Integrity Verification
|
|
46
|
+
* @micro Calculate and compare v2-core file hashes against manifest
|
|
47
|
+
* @inputs $MANIFEST_FILE: string — Path to .spine-manifest.json
|
|
48
|
+
* @outputs exit code — 0 for pass, 1 for failure
|
|
49
|
+
* @depends-on [find, sha256sum, jq/grep, .spine-manifest.json]
|
|
50
|
+
* @depended-by [CI/CD pipelines, pre-deploy hooks]
|
|
51
|
+
* @side-effects [console output, exit with status code]
|
|
52
|
+
* @tags integrity, verification, security, v2-core
|
|
53
|
+
*/
|
|
54
|
+
# Calculate current hashes
|
|
55
|
+
CURRENT_SRC_HASH=$(find "$CORE_DIR/src" -type f -name '*.tsx' -o -name '*.ts' -o -name '*.css' 2>/dev/null | LC_ALL=C sort | xargs cat 2>/dev/null | sha256sum | cut -d' ' -f1)
|
|
56
|
+
CURRENT_FUNCTIONS_HASH=$(find "$CORE_DIR/functions" -type f -name '*.ts' 2>/dev/null | LC_ALL=C sort | xargs cat 2>/dev/null | sha256sum | cut -d' ' -f1)
|
|
57
|
+
|
|
58
|
+
# Extract expected hashes from manifest (using jq if available, fallback to grep)
|
|
59
|
+
if command -v jq >/dev/null 2>&1; then
|
|
60
|
+
EXPECTED_SRC_HASH=$(jq -r '.integrity.src' "$MANIFEST_FILE" | sed 's/sha256-//')
|
|
61
|
+
EXPECTED_FUNCTIONS_HASH=$(jq -r '.integrity.functions' "$MANIFEST_FILE" | sed 's/sha256-//')
|
|
62
|
+
else
|
|
63
|
+
EXPECTED_SRC_HASH=$(grep '"src"' "$MANIFEST_FILE" | cut -d'"' -f4 | sed 's/sha256-//')
|
|
64
|
+
EXPECTED_FUNCTIONS_HASH=$(grep '"functions"' "$MANIFEST_FILE" | cut -d'"' -f4 | sed 's/sha256-//')
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# Check integrity
|
|
68
|
+
if [ "$CURRENT_SRC_HASH" = "$EXPECTED_SRC_HASH" ] && [ "$CURRENT_FUNCTIONS_HASH" = "$EXPECTED_FUNCTIONS_HASH" ]; then
|
|
69
|
+
echo " Integrity check PASSED - Core files are unchanged"
|
|
70
|
+
exit 0
|
|
71
|
+
else
|
|
72
|
+
echo " INTEGRITY CHECK FAILED - Core files have been modified!"
|
|
73
|
+
echo " WARRANTY VOIDED - Core tampering detected"
|
|
74
|
+
echo ""
|
|
75
|
+
echo " Expected src hash: $EXPECTED_SRC_HASH"
|
|
76
|
+
echo " Current src hash: $CURRENT_SRC_HASH"
|
|
77
|
+
echo ""
|
|
78
|
+
echo " Expected functions hash: $EXPECTED_FUNCTIONS_HASH"
|
|
79
|
+
echo " Current functions hash: $CURRENT_FUNCTIONS_HASH"
|
|
80
|
+
exit 1
|
|
81
|
+
fi
|
|
82
|
+
// ─── CHUNK_END: CHECK_CORE_INTEGRITY_MAIN ────────────────────────────────────────────────
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const querystring = require('querystring');
|
|
6
|
+
|
|
7
|
+
// Configuration - use local Netlify Functions
|
|
8
|
+
const NETLIFY_URL = 'http://localhost:8888';
|
|
9
|
+
const SUPABASE_SERVICE_ROLE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InV5b2t1aWlienR3YXNkcHJzb3YiLCJyb2xlIjoic2VydmljZV9yb2xlIiwiaWF0IjoxNzM2MTY3Njk5LCJleHAiOjIwNTE3NDM2OTl9.qKqJv8N-IY9q8o_XJtq2o5s3RfD2W4x7J8L9q8o_XJt';
|
|
10
|
+
|
|
11
|
+
// Read chunks from file
|
|
12
|
+
function readChunks(filePath) {
|
|
13
|
+
try {
|
|
14
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
15
|
+
const data = JSON.parse(content);
|
|
16
|
+
return data.chunks || [];
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error('Error reading chunks file:', error.message);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Make HTTP request to local Netlify Function
|
|
24
|
+
function makeRequest(functionName, data) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const postData = JSON.stringify(data);
|
|
27
|
+
const url = `${NETLIFY_URL}/.netlify/functions/${functionName}`;
|
|
28
|
+
|
|
29
|
+
console.log(` 🌐 Calling: ${url}`);
|
|
30
|
+
console.log(` 📦 Data size: ${postData.length} bytes`);
|
|
31
|
+
|
|
32
|
+
const options = {
|
|
33
|
+
hostname: 'localhost',
|
|
34
|
+
port: 8888,
|
|
35
|
+
path: `/.netlify/functions/${functionName}`,
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: {
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
'Authorization': `Bearer ${SUPABASE_SERVICE_ROLE_KEY}`,
|
|
40
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const req = require('http').request(options, (res) => {
|
|
45
|
+
let responseData = '';
|
|
46
|
+
|
|
47
|
+
res.on('data', (chunk) => {
|
|
48
|
+
responseData += chunk;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
res.on('end', () => {
|
|
52
|
+
console.log(` 📡 Response status: ${res.statusCode}`);
|
|
53
|
+
console.log(` 📄 Response body: ${responseData.substring(0, 200)}${responseData.length > 200 ? '...' : ''}`);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(responseData);
|
|
57
|
+
resolve({ status: res.statusCode, data: parsed });
|
|
58
|
+
} catch (error) {
|
|
59
|
+
resolve({ status: res.statusCode, data: responseData });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
req.on('error', (error) => {
|
|
65
|
+
console.log(` ❌ Request error: ${error.message}`);
|
|
66
|
+
reject(error);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
req.write(postData);
|
|
70
|
+
req.end();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Ingest chunks in batches to avoid timeouts
|
|
75
|
+
async function ingestChunks(chunks, batchSize = 10) {
|
|
76
|
+
const results = {
|
|
77
|
+
total: chunks.length,
|
|
78
|
+
processed: 0,
|
|
79
|
+
created: 0,
|
|
80
|
+
updated: 0,
|
|
81
|
+
errors: []
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
console.log(`Starting ingestion of ${chunks.length} chunks in batches of ${batchSize}...`);
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < chunks.length; i += batchSize) {
|
|
87
|
+
const batch = chunks.slice(i, i + batchSize);
|
|
88
|
+
const batchNum = Math.floor(i / batchSize) + 1;
|
|
89
|
+
const totalBatches = Math.ceil(chunks.length / batchSize);
|
|
90
|
+
|
|
91
|
+
console.log(`Processing batch ${batchNum}/${totalBatches} (${batch.length} chunks)...`);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const response = await makeRequest('custom_kb-ingestion', {
|
|
95
|
+
chunks: batch,
|
|
96
|
+
force_update: false
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (response.status === 200) {
|
|
100
|
+
const result = response.data;
|
|
101
|
+
results.processed += batch.length;
|
|
102
|
+
results.created += result.items_created || 0;
|
|
103
|
+
results.updated += result.items_updated || 0;
|
|
104
|
+
|
|
105
|
+
if (result.errors && result.errors.length > 0) {
|
|
106
|
+
results.errors.push(...result.errors);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (result.skipped && result.skipped.length > 0) {
|
|
110
|
+
console.log(` Batch ${batchNum}: ${result.items_created || 0} created, ${result.items_updated || 0} updated, ${result.skipped?.length || 0} skipped`);
|
|
111
|
+
} else {
|
|
112
|
+
console.log(` Batch ${batchNum}: ${result.items_created || 0} created, ${result.items_updated || 0} updated`);
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
const error = `HTTP ${response.status}: ${JSON.stringify(response.data)}`;
|
|
116
|
+
results.errors.push(`Batch ${batchNum}: ${error}`);
|
|
117
|
+
console.error(` Batch ${batchNum} failed: ${error}`);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const batchError = `Batch ${batchNum}: ${error.message}`;
|
|
121
|
+
results.errors.push(batchError);
|
|
122
|
+
console.error(` Batch ${batchNum} failed: ${error.message}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Small delay between batches to avoid rate limiting
|
|
126
|
+
if (i + batchSize < chunks.length) {
|
|
127
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return results;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Main function
|
|
135
|
+
async function main() {
|
|
136
|
+
const args = process.argv.slice(2);
|
|
137
|
+
|
|
138
|
+
if (args.length < 1) {
|
|
139
|
+
console.log('Usage: node ingest-chunks.js <chunks.json>');
|
|
140
|
+
console.log('Example: node ingest-chunks.js chunks.json');
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const chunksFile = args[0];
|
|
145
|
+
|
|
146
|
+
if (!fs.existsSync(chunksFile)) {
|
|
147
|
+
console.error(`Chunks file not found: ${chunksFile}`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const chunks = readChunks(chunksFile);
|
|
153
|
+
console.log(`Loaded ${chunks.length} chunks from ${chunksFile}`);
|
|
154
|
+
|
|
155
|
+
// Show sample chunk
|
|
156
|
+
if (chunks.length > 0) {
|
|
157
|
+
console.log('\nSample chunk:');
|
|
158
|
+
console.log(`- ID: ${chunks[0].chunk_id}`);
|
|
159
|
+
console.log(`- Macro: ${chunks[0].macro}`);
|
|
160
|
+
console.log(`- Micro: ${chunks[0].micro}`);
|
|
161
|
+
console.log(`- File: ${chunks[0].metadata.file_path}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Ask for confirmation
|
|
165
|
+
console.log('\nReady to ingest chunks into KB system.');
|
|
166
|
+
console.log('This will create KB articles and embeddings for each chunk.');
|
|
167
|
+
console.log('Continue? (y/N)');
|
|
168
|
+
|
|
169
|
+
// For automation, we'll proceed without interactive confirmation
|
|
170
|
+
console.log('Proceeding with ingestion...\n');
|
|
171
|
+
|
|
172
|
+
const results = await ingestChunks(chunks);
|
|
173
|
+
|
|
174
|
+
console.log('\n=== Ingestion Complete ===');
|
|
175
|
+
console.log(`Total chunks: ${results.total}`);
|
|
176
|
+
console.log(`Processed: ${results.processed}`);
|
|
177
|
+
console.log(`Created: ${results.created}`);
|
|
178
|
+
console.log(`Updated: ${results.updated}`);
|
|
179
|
+
console.log(`Errors: ${results.errors.length}`);
|
|
180
|
+
|
|
181
|
+
if (results.errors.length > 0) {
|
|
182
|
+
console.log('\nErrors:');
|
|
183
|
+
results.errors.slice(0, 10).forEach((error, i) => {
|
|
184
|
+
console.log(`${i + 1}. ${error}`);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (results.errors.length > 10) {
|
|
188
|
+
console.log(`... and ${results.errors.length - 10} more errors`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log('\nYou can now check the KB articles in your Spine instance!');
|
|
193
|
+
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error('Fatal error:', error.message);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (require.main === module) {
|
|
201
|
+
main();
|
|
202
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
class KBChunkParser {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.chunks = [];
|
|
9
|
+
this.currentChunk = {};
|
|
10
|
+
this.inChunk = false;
|
|
11
|
+
this.currentFile = '';
|
|
12
|
+
this.currentLine = 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async parseDirectory(dirPath) {
|
|
16
|
+
this.chunks = [];
|
|
17
|
+
|
|
18
|
+
const walkDir = (dir, fileList = []) => {
|
|
19
|
+
const files = fs.readdirSync(dir);
|
|
20
|
+
|
|
21
|
+
for (const file of files) {
|
|
22
|
+
const filePath = path.join(dir, file);
|
|
23
|
+
const stat = fs.statSync(filePath);
|
|
24
|
+
|
|
25
|
+
if (stat.isDirectory()) {
|
|
26
|
+
// Skip certain directories
|
|
27
|
+
if (!['node_modules', '.git', 'dist', 'src/v2-assembled'].includes(file)) {
|
|
28
|
+
walkDir(filePath, fileList);
|
|
29
|
+
}
|
|
30
|
+
} else if (file.match(/\.(ts|js|tsx|jsx)$/)) {
|
|
31
|
+
fileList.push(filePath);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return fileList;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const files = walkDir(dirPath);
|
|
39
|
+
|
|
40
|
+
for (const filePath of files) {
|
|
41
|
+
await this.parseFile(filePath);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return this.chunks;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async parseFile(filePath) {
|
|
48
|
+
this.currentFile = filePath;
|
|
49
|
+
this.currentLine = 0;
|
|
50
|
+
this.inChunk = false;
|
|
51
|
+
this.currentChunk = {};
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
55
|
+
const lines = content.split('\n');
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < lines.length; i++) {
|
|
58
|
+
this.currentLine = i + 1;
|
|
59
|
+
const line = lines[i];
|
|
60
|
+
|
|
61
|
+
if (line.includes('CHUNK_START:')) {
|
|
62
|
+
this.handleChunkStart(line);
|
|
63
|
+
} else if (line.includes('CHUNK_END:')) {
|
|
64
|
+
this.handleChunkEnd();
|
|
65
|
+
} else if (this.inChunk && this.currentChunk.code !== undefined) {
|
|
66
|
+
this.currentChunk.code += line + '\n';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error(`Error parsing file ${filePath}:`, error.message);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
handleChunkStart(line) {
|
|
75
|
+
const match = line.match(/CHUNK_START:\s*(.+?)\s*─/);
|
|
76
|
+
if (!match) {
|
|
77
|
+
console.warn(`Invalid CHUNK_START format at line ${this.currentLine}: ${line}`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const identifier = match[1].trim();
|
|
82
|
+
this.inChunk = true;
|
|
83
|
+
this.currentChunk = {
|
|
84
|
+
identifier,
|
|
85
|
+
code: '',
|
|
86
|
+
metadata: {
|
|
87
|
+
chunk_id: '',
|
|
88
|
+
file_path: this.currentFile,
|
|
89
|
+
line_start: this.currentLine,
|
|
90
|
+
line_end: 0,
|
|
91
|
+
chunk_type: 'function',
|
|
92
|
+
purpose: '',
|
|
93
|
+
hash: '',
|
|
94
|
+
dependencies: [],
|
|
95
|
+
dependents: [],
|
|
96
|
+
source: {
|
|
97
|
+
source_type: 'core',
|
|
98
|
+
ref: this.currentFile,
|
|
99
|
+
line_start: this.currentLine,
|
|
100
|
+
line_end: 0
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
handleChunkEnd() {
|
|
107
|
+
if (!this.inChunk) {
|
|
108
|
+
console.warn(`CHUNK_END without CHUNK_START at line ${this.currentLine}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const match = this.currentChunk.code?.match(/@chunk-id\s+(.+)/);
|
|
113
|
+
if (!match) {
|
|
114
|
+
console.warn(`Missing @chunk-id in chunk ${this.currentChunk.identifier}`);
|
|
115
|
+
this.inChunk = false;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const chunkId = match[1].trim();
|
|
120
|
+
const hash = this.generateHash(this.currentChunk.code || '');
|
|
121
|
+
|
|
122
|
+
// Parse JSDoc-style comments
|
|
123
|
+
const parsed = this.parseChunkDoc(this.currentChunk.code || '');
|
|
124
|
+
|
|
125
|
+
const chunk = {
|
|
126
|
+
identifier: this.currentChunk.identifier,
|
|
127
|
+
chunk_id: chunkId,
|
|
128
|
+
version: '1.0.0',
|
|
129
|
+
hash,
|
|
130
|
+
macro: parsed.macro || '',
|
|
131
|
+
micro: parsed.micro || '',
|
|
132
|
+
inputs: parsed.inputs || {},
|
|
133
|
+
outputs: parsed.outputs || '',
|
|
134
|
+
depends_on: parsed.depends_on || [],
|
|
135
|
+
depended_by: parsed.depended_by || [],
|
|
136
|
+
side_effects: parsed.side_effects || [],
|
|
137
|
+
tags: parsed.tags || [],
|
|
138
|
+
code: (this.currentChunk.code || '').trim(),
|
|
139
|
+
metadata: {
|
|
140
|
+
...this.currentChunk.metadata,
|
|
141
|
+
chunk_id: chunkId,
|
|
142
|
+
hash,
|
|
143
|
+
line_end: this.currentLine,
|
|
144
|
+
source: {
|
|
145
|
+
...this.currentChunk.metadata.source,
|
|
146
|
+
line_end: this.currentLine
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
this.chunks.push(chunk);
|
|
152
|
+
this.inChunk = false;
|
|
153
|
+
this.currentChunk = {};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
parseChunkDoc(code) {
|
|
157
|
+
const doc = {};
|
|
158
|
+
|
|
159
|
+
// Extract @chunk-id
|
|
160
|
+
const chunkIdMatch = code.match(/@chunk-id\s+(.+)/);
|
|
161
|
+
if (chunkIdMatch) {
|
|
162
|
+
doc.chunk_id = chunkIdMatch[1].trim();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Extract @version
|
|
166
|
+
const versionMatch = code.match(/@version\s+(.+)/);
|
|
167
|
+
if (versionMatch) {
|
|
168
|
+
doc.version = versionMatch[1].trim();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Extract @hash
|
|
172
|
+
const hashMatch = code.match(/@hash\s+(.+)/);
|
|
173
|
+
if (hashMatch) {
|
|
174
|
+
doc.hash = hashMatch[1].trim();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Extract @macro
|
|
178
|
+
const macroMatch = code.match(/@macro\s+(.+)/);
|
|
179
|
+
if (macroMatch) {
|
|
180
|
+
doc.macro = macroMatch[1].trim();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Extract @micro
|
|
184
|
+
const microMatch = code.match(/@micro\s+(.+)/);
|
|
185
|
+
if (microMatch) {
|
|
186
|
+
doc.micro = microMatch[1].trim();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Extract @inputs
|
|
190
|
+
const inputsMatch = code.match(/@inputs\s+(.+)/);
|
|
191
|
+
if (inputsMatch) {
|
|
192
|
+
doc.inputs = this.parseKeyValuePairs(inputsMatch[1].trim());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Extract @outputs
|
|
196
|
+
const outputsMatch = code.match(/@outputs\s+(.+)/);
|
|
197
|
+
if (outputsMatch) {
|
|
198
|
+
doc.outputs = outputsMatch[1].trim();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Extract @depends-on
|
|
202
|
+
const dependsOnMatch = code.match(/@depends-on\s+(.+)/);
|
|
203
|
+
if (dependsOnMatch) {
|
|
204
|
+
doc.depends_on = this.parseList(dependsOnMatch[1].trim());
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Extract @depended-by
|
|
208
|
+
const dependedByMatch = code.match(/@depended-by\s+(.+)/);
|
|
209
|
+
if (dependedByMatch) {
|
|
210
|
+
doc.depended_by = this.parseList(dependedByMatch[1].trim());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Extract @side-effects
|
|
214
|
+
const sideEffectsMatch = code.match(/@side-effects\s+(.+)/);
|
|
215
|
+
if (sideEffectsMatch) {
|
|
216
|
+
doc.side_effects = this.parseList(sideEffectsMatch[1].trim());
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Extract @tags
|
|
220
|
+
const tagsMatch = code.match(/@tags\s+(.+)/);
|
|
221
|
+
if (tagsMatch) {
|
|
222
|
+
doc.tags = this.parseList(tagsMatch[1].trim());
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return doc;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
parseKeyValuePairs(str) {
|
|
229
|
+
const pairs = {};
|
|
230
|
+
const regex = /(\w+):\s*([^,]+)(?:,\s*|$)/g;
|
|
231
|
+
let match;
|
|
232
|
+
|
|
233
|
+
while ((match = regex.exec(str)) !== null) {
|
|
234
|
+
pairs[match[1]] = match[2].trim();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return pairs;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
parseList(str) {
|
|
241
|
+
return str.split(',').map(s => s.trim()).filter(Boolean);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
generateHash(content) {
|
|
245
|
+
// Simple hash function for demonstration
|
|
246
|
+
let hash = 0;
|
|
247
|
+
for (let i = 0; i < content.length; i++) {
|
|
248
|
+
const char = content.charCodeAt(i);
|
|
249
|
+
hash = ((hash << 5) - hash) + char;
|
|
250
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
251
|
+
}
|
|
252
|
+
return Math.abs(hash).toString(16);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async saveChunks(outputFile) {
|
|
256
|
+
const output = {
|
|
257
|
+
generated_at: new Date().toISOString(),
|
|
258
|
+
total_chunks: this.chunks.length,
|
|
259
|
+
chunks: this.chunks
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
fs.writeFileSync(outputFile, JSON.stringify(output, null, 2));
|
|
263
|
+
console.log(`Saved ${this.chunks.length} chunks to ${outputFile}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// CLI usage
|
|
268
|
+
async function main() {
|
|
269
|
+
const args = process.argv.slice(2);
|
|
270
|
+
|
|
271
|
+
if (args.length < 2) {
|
|
272
|
+
console.log('Usage: node kb-chunk-parser.js <source-dir> <output-file>');
|
|
273
|
+
console.log('Example: node kb-chunk-parser.js v2-core/functions chunks.json');
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const [sourceDir, outputFile] = args;
|
|
278
|
+
|
|
279
|
+
if (!fs.existsSync(sourceDir)) {
|
|
280
|
+
console.error(`Source directory not found: ${sourceDir}`);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const parser = new KBChunkParser();
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
console.log(`Parsing chunks from ${sourceDir}...`);
|
|
288
|
+
const chunks = await parser.parseDirectory(sourceDir);
|
|
289
|
+
|
|
290
|
+
console.log(`Found ${chunks.length} chunks`);
|
|
291
|
+
|
|
292
|
+
await parser.saveChunks(outputFile);
|
|
293
|
+
|
|
294
|
+
// Display summary
|
|
295
|
+
console.log('\nSummary:');
|
|
296
|
+
console.log(`- Functions: ${chunks.filter(c => c.metadata.chunk_type === 'function').length}`);
|
|
297
|
+
console.log(`- Classes: ${chunks.filter(c => c.metadata.chunk_type === 'class').length}`);
|
|
298
|
+
console.log(`- Interfaces: ${chunks.filter(c => c.metadata.chunk_type === 'interface').length}`);
|
|
299
|
+
console.log(`- Configs: ${chunks.filter(c => c.metadata.chunk_type === 'config').length}`);
|
|
300
|
+
console.log(`- Objects: ${chunks.filter(c => c.metadata.chunk_type === 'object').length}`);
|
|
301
|
+
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error('Error:', error.message);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (require.main === module) {
|
|
309
|
+
main();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
module.exports = { KBChunkParser };
|