monomind 1.10.16 → 1.10.17
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 +4 -1
- package/packages/@monomind/cli/package.json +1 -1
- package/packages/@monomind/cli/scripts/deploy-ipfs-node.sh +153 -0
- package/packages/@monomind/cli/scripts/publish.sh +55 -0
- package/packages/@monomind/cli/scripts/sync-claude-assets.sh +34 -0
- package/packages/@monomind/cli/scripts/understand-analyze.mjs +934 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "monomind",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.17",
|
|
4
4
|
"description": "Monomind - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"packages/@monomind/cli/dist/**/*.d.ts",
|
|
24
24
|
"!packages/@monomind/cli/dist/**/*.map",
|
|
25
25
|
"packages/@monomind/cli/bundled-graph/**",
|
|
26
|
+
"packages/@monomind/cli/scripts/**/*.mjs",
|
|
27
|
+
"packages/@monomind/cli/scripts/**/*.js",
|
|
28
|
+
"packages/@monomind/cli/scripts/**/*.sh",
|
|
26
29
|
"packages/@monomind/cli/package.json",
|
|
27
30
|
"packages/@monomind/shared/dist/**/*.js",
|
|
28
31
|
"packages/@monomind/shared/dist/**/*.d.ts",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoes/monomindcli",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Monomind CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Deploy IPFS Node to Google Cloud
|
|
4
|
+
# Provides free IPFS pinning for your users
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# ./deploy-ipfs-node.sh [PROJECT_ID] [ZONE]
|
|
8
|
+
#
|
|
9
|
+
# Example:
|
|
10
|
+
# ./deploy-ipfs-node.sh my-project us-central1-a
|
|
11
|
+
#
|
|
12
|
+
|
|
13
|
+
set -e
|
|
14
|
+
|
|
15
|
+
PROJECT_ID="${1:-$(gcloud config get-value project)}"
|
|
16
|
+
ZONE="${2:-us-central1-a}"
|
|
17
|
+
INSTANCE_NAME="ipfs-node"
|
|
18
|
+
MACHINE_TYPE="e2-medium" # $25/month, use e2-small for $8/month
|
|
19
|
+
DISK_SIZE="100GB"
|
|
20
|
+
|
|
21
|
+
echo "╔══════════════════════════════════════════════════════════════╗"
|
|
22
|
+
echo "║ IPFS Node Deployment for Monomind ║"
|
|
23
|
+
echo "╚══════════════════════════════════════════════════════════════╝"
|
|
24
|
+
echo ""
|
|
25
|
+
echo "Project: $PROJECT_ID"
|
|
26
|
+
echo "Zone: $ZONE"
|
|
27
|
+
echo "Machine: $MACHINE_TYPE"
|
|
28
|
+
echo "Disk: $DISK_SIZE"
|
|
29
|
+
echo ""
|
|
30
|
+
|
|
31
|
+
# Create firewall rules
|
|
32
|
+
echo "▶ Creating firewall rules..."
|
|
33
|
+
gcloud compute firewall-rules create ipfs-swarm \
|
|
34
|
+
--project="$PROJECT_ID" \
|
|
35
|
+
--allow=tcp:4001,udp:4001 \
|
|
36
|
+
--target-tags=ipfs-node \
|
|
37
|
+
--description="IPFS swarm connections" \
|
|
38
|
+
2>/dev/null || echo " (firewall rule already exists)"
|
|
39
|
+
|
|
40
|
+
gcloud compute firewall-rules create ipfs-api \
|
|
41
|
+
--project="$PROJECT_ID" \
|
|
42
|
+
--allow=tcp:5001 \
|
|
43
|
+
--target-tags=ipfs-node \
|
|
44
|
+
--source-ranges="0.0.0.0/0" \
|
|
45
|
+
--description="IPFS API (consider restricting)" \
|
|
46
|
+
2>/dev/null || echo " (firewall rule already exists)"
|
|
47
|
+
|
|
48
|
+
gcloud compute firewall-rules create ipfs-gateway \
|
|
49
|
+
--project="$PROJECT_ID" \
|
|
50
|
+
--allow=tcp:8080 \
|
|
51
|
+
--target-tags=ipfs-node \
|
|
52
|
+
--description="IPFS Gateway" \
|
|
53
|
+
2>/dev/null || echo " (firewall rule already exists)"
|
|
54
|
+
|
|
55
|
+
# Create startup script
|
|
56
|
+
STARTUP_SCRIPT='#!/bin/bash
|
|
57
|
+
set -e
|
|
58
|
+
|
|
59
|
+
# Install IPFS
|
|
60
|
+
echo "Installing IPFS..."
|
|
61
|
+
wget -q https://dist.ipfs.tech/kubo/v0.24.0/kubo_v0.24.0_linux-amd64.tar.gz
|
|
62
|
+
tar xzf kubo_v0.24.0_linux-amd64.tar.gz
|
|
63
|
+
cd kubo && sudo bash install.sh
|
|
64
|
+
cd .. && rm -rf kubo kubo_v0.24.0_linux-amd64.tar.gz
|
|
65
|
+
|
|
66
|
+
# Create ipfs user
|
|
67
|
+
sudo useradd -m -s /bin/bash ipfs || true
|
|
68
|
+
|
|
69
|
+
# Initialize IPFS
|
|
70
|
+
sudo -u ipfs IPFS_PATH=/home/ipfs/.ipfs ipfs init --profile=server
|
|
71
|
+
|
|
72
|
+
# Configure IPFS for server use
|
|
73
|
+
sudo -u ipfs IPFS_PATH=/home/ipfs/.ipfs ipfs config Addresses.API /ip4/0.0.0.0/tcp/5001
|
|
74
|
+
sudo -u ipfs IPFS_PATH=/home/ipfs/.ipfs ipfs config Addresses.Gateway /ip4/0.0.0.0/tcp/8080
|
|
75
|
+
sudo -u ipfs IPFS_PATH=/home/ipfs/.ipfs ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin "[\"*\"]"
|
|
76
|
+
sudo -u ipfs IPFS_PATH=/home/ipfs/.ipfs ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods "[\"PUT\", \"POST\", \"GET\"]"
|
|
77
|
+
|
|
78
|
+
# Set storage limits (adjust as needed)
|
|
79
|
+
sudo -u ipfs IPFS_PATH=/home/ipfs/.ipfs ipfs config Datastore.StorageMax 80GB
|
|
80
|
+
|
|
81
|
+
# Create systemd service
|
|
82
|
+
cat > /etc/systemd/system/ipfs.service << EOF
|
|
83
|
+
[Unit]
|
|
84
|
+
Description=IPFS Daemon
|
|
85
|
+
After=network.target
|
|
86
|
+
|
|
87
|
+
[Service]
|
|
88
|
+
Type=simple
|
|
89
|
+
User=ipfs
|
|
90
|
+
Environment=IPFS_PATH=/home/ipfs/.ipfs
|
|
91
|
+
ExecStart=/usr/local/bin/ipfs daemon --migrate=true
|
|
92
|
+
Restart=on-failure
|
|
93
|
+
RestartSec=10
|
|
94
|
+
|
|
95
|
+
[Install]
|
|
96
|
+
WantedBy=multi-user.target
|
|
97
|
+
EOF
|
|
98
|
+
|
|
99
|
+
# Start IPFS
|
|
100
|
+
systemctl daemon-reload
|
|
101
|
+
systemctl enable ipfs
|
|
102
|
+
systemctl start ipfs
|
|
103
|
+
|
|
104
|
+
echo "IPFS node started successfully!"
|
|
105
|
+
'
|
|
106
|
+
|
|
107
|
+
# Create instance
|
|
108
|
+
echo "▶ Creating VM instance..."
|
|
109
|
+
gcloud compute instances create "$INSTANCE_NAME" \
|
|
110
|
+
--project="$PROJECT_ID" \
|
|
111
|
+
--zone="$ZONE" \
|
|
112
|
+
--machine-type="$MACHINE_TYPE" \
|
|
113
|
+
--image-family=ubuntu-2204-lts \
|
|
114
|
+
--image-project=ubuntu-os-cloud \
|
|
115
|
+
--boot-disk-size="$DISK_SIZE" \
|
|
116
|
+
--boot-disk-type=pd-ssd \
|
|
117
|
+
--tags=ipfs-node \
|
|
118
|
+
--metadata=startup-script="$STARTUP_SCRIPT"
|
|
119
|
+
|
|
120
|
+
# Get external IP
|
|
121
|
+
echo ""
|
|
122
|
+
echo "▶ Waiting for instance to start..."
|
|
123
|
+
sleep 30
|
|
124
|
+
|
|
125
|
+
EXTERNAL_IP=$(gcloud compute instances describe "$INSTANCE_NAME" \
|
|
126
|
+
--project="$PROJECT_ID" \
|
|
127
|
+
--zone="$ZONE" \
|
|
128
|
+
--format='get(networkInterfaces[0].accessConfigs[0].natIP)')
|
|
129
|
+
|
|
130
|
+
echo ""
|
|
131
|
+
echo "═══════════════════════════════════════════════════════════════"
|
|
132
|
+
echo " DEPLOYMENT COMPLETE"
|
|
133
|
+
echo "═══════════════════════════════════════════════════════════════"
|
|
134
|
+
echo ""
|
|
135
|
+
echo " IPFS Node IP: $EXTERNAL_IP"
|
|
136
|
+
echo ""
|
|
137
|
+
echo " Endpoints:"
|
|
138
|
+
echo " API: http://$EXTERNAL_IP:5001"
|
|
139
|
+
echo " Gateway: http://$EXTERNAL_IP:8080"
|
|
140
|
+
echo " Swarm: /ip4/$EXTERNAL_IP/tcp/4001"
|
|
141
|
+
echo ""
|
|
142
|
+
echo " Test commands:"
|
|
143
|
+
echo " curl http://$EXTERNAL_IP:5001/api/v0/id"
|
|
144
|
+
echo " curl http://$EXTERNAL_IP:8080/ipfs/QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG/readme"
|
|
145
|
+
echo ""
|
|
146
|
+
echo " Configure Monomind CLI:"
|
|
147
|
+
echo " export IPFS_API_URL=http://$EXTERNAL_IP:5001"
|
|
148
|
+
echo ""
|
|
149
|
+
echo " SSH into node:"
|
|
150
|
+
echo " gcloud compute ssh $INSTANCE_NAME --zone=$ZONE"
|
|
151
|
+
echo ""
|
|
152
|
+
echo " Monthly cost estimate: ~\$25-54 depending on usage"
|
|
153
|
+
echo ""
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Publish script for @monomind/cli
|
|
3
|
+
# Publishes to both @monomind/cli@alpha AND monomind@alpha
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
CLI_DIR="$(dirname "$SCRIPT_DIR")"
|
|
9
|
+
|
|
10
|
+
cd "$CLI_DIR"
|
|
11
|
+
|
|
12
|
+
# Get current version
|
|
13
|
+
VERSION=$(node -p "require('./package.json').version")
|
|
14
|
+
echo "Publishing version: $VERSION"
|
|
15
|
+
|
|
16
|
+
# 1. Publish @monomind/cli with alpha tag
|
|
17
|
+
echo ""
|
|
18
|
+
echo "=== Publishing @monomind/cli@$VERSION (alpha tag) ==="
|
|
19
|
+
npm publish --tag alpha
|
|
20
|
+
|
|
21
|
+
# 2. Publish to monomind with alpha tag
|
|
22
|
+
echo ""
|
|
23
|
+
echo "=== Publishing monomind@$VERSION (alpha tag) ==="
|
|
24
|
+
|
|
25
|
+
# Create temp directory
|
|
26
|
+
TEMP_DIR=$(mktemp -d)
|
|
27
|
+
trap "rm -rf $TEMP_DIR" EXIT
|
|
28
|
+
|
|
29
|
+
# Copy necessary files
|
|
30
|
+
cp -r dist bin src package.json README.md "$TEMP_DIR/"
|
|
31
|
+
|
|
32
|
+
# Change package name to unscoped
|
|
33
|
+
cd "$TEMP_DIR"
|
|
34
|
+
sed -i 's/"name": "@monomind\/cli"/"name": "monomind"/' package.json
|
|
35
|
+
|
|
36
|
+
# Publish with alpha tag
|
|
37
|
+
npm publish --tag alpha
|
|
38
|
+
|
|
39
|
+
echo ""
|
|
40
|
+
echo "=== Updating dist-tags ==="
|
|
41
|
+
|
|
42
|
+
# Update all tags to point to the new version
|
|
43
|
+
npm dist-tag add @monomind/cli@$VERSION alpha
|
|
44
|
+
npm dist-tag add @monomind/cli@$VERSION latest
|
|
45
|
+
npm dist-tag add monomind@$VERSION alpha
|
|
46
|
+
npm dist-tag add monomind@$VERSION latest
|
|
47
|
+
|
|
48
|
+
echo ""
|
|
49
|
+
echo "=== Published successfully ==="
|
|
50
|
+
echo " @monomind/cli@$VERSION (alpha, latest)"
|
|
51
|
+
echo " monomind@$VERSION (alpha, latest)"
|
|
52
|
+
echo ""
|
|
53
|
+
echo "Install with:"
|
|
54
|
+
echo " npx monomind@alpha"
|
|
55
|
+
echo " npx @monomind/cli@latest"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Syncs distributable .claude assets (skills, commands, agents, helpers) from
|
|
3
|
+
# the monorepo root into this package so they're included in the npm tarball.
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
PKG_DIR="$SCRIPT_DIR/.."
|
|
8
|
+
ROOT_DIR="$PKG_DIR/../../.."
|
|
9
|
+
SRC="$ROOT_DIR/.claude"
|
|
10
|
+
DEST="$PKG_DIR/.claude"
|
|
11
|
+
|
|
12
|
+
mkdir -p "$DEST"
|
|
13
|
+
|
|
14
|
+
for dir in skills commands agents helpers; do
|
|
15
|
+
if [ -d "$SRC/$dir" ]; then
|
|
16
|
+
rm -rf "$DEST/$dir"
|
|
17
|
+
# Copy while excluding runtime .monomind subdirectories
|
|
18
|
+
rsync -a --exclude='.monomind/' "$SRC/$dir/" "$DEST/$dir/"
|
|
19
|
+
echo " synced .claude/$dir"
|
|
20
|
+
fi
|
|
21
|
+
done
|
|
22
|
+
|
|
23
|
+
# Bundle @monomind/graph dist for npm installs
|
|
24
|
+
GRAPH_SRC="$ROOT_DIR/packages/@monomind/graph"
|
|
25
|
+
GRAPH_DEST="$PKG_DIR/bundled-graph"
|
|
26
|
+
if [ -d "$GRAPH_SRC/dist" ]; then
|
|
27
|
+
rm -rf "$GRAPH_DEST"
|
|
28
|
+
mkdir -p "$GRAPH_DEST"
|
|
29
|
+
cp -r "$GRAPH_SRC/dist" "$GRAPH_DEST/dist"
|
|
30
|
+
cp "$GRAPH_SRC/package.json" "$GRAPH_DEST/package.json"
|
|
31
|
+
echo " synced bundled-graph"
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
echo "sync-claude-assets: done"
|
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* understand-analyze.mjs — Built-in semantic enrichment engine for monomind:understand
|
|
4
|
+
*
|
|
5
|
+
* Ported from Understand-Anything (understand-anything-plugin/packages/core).
|
|
6
|
+
* Ships inside @monomind/cli — no external plugin needed.
|
|
7
|
+
*
|
|
8
|
+
* Reads file nodes from monograph.db, calls the Anthropic API to generate
|
|
9
|
+
* summaries, tags, complexity, and architectural layers, then writes results
|
|
10
|
+
* back into the DB (and optionally emits a graph.json).
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node understand-analyze.mjs [options]
|
|
14
|
+
*
|
|
15
|
+
* Options:
|
|
16
|
+
* --dir <path> Project directory (default: cwd)
|
|
17
|
+
* --db <path> monograph.db path (default: <dir>/.monomind/monograph.db)
|
|
18
|
+
* --output <path> Write a graph.json here (default: <dir>/.understand/knowledge-graph.json)
|
|
19
|
+
* --batch-size <N> Files per LLM batch (default: 5)
|
|
20
|
+
* --max-files <N> Stop after N files (0 = all, default: 0)
|
|
21
|
+
* --dry-run Print what would happen without writing to DB
|
|
22
|
+
* --no-llm Heuristic-only mode (layers + tags from paths, no API calls)
|
|
23
|
+
* --layers-only Skip per-file analysis, only (re-)detect layers
|
|
24
|
+
*
|
|
25
|
+
* Env:
|
|
26
|
+
* ANTHROPIC_API_KEY Required unless --no-llm is set
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
30
|
+
import { resolve, join, dirname, basename, relative } from 'node:path';
|
|
31
|
+
import { createRequire } from 'node:module';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
import { execFileSync } from 'node:child_process';
|
|
34
|
+
|
|
35
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
const CWD = process.cwd();
|
|
37
|
+
|
|
38
|
+
// ── CLI argument helpers ─────────────────────────────────────────────────────
|
|
39
|
+
function argVal(name) {
|
|
40
|
+
const i = process.argv.indexOf('--' + name);
|
|
41
|
+
return i !== -1 ? process.argv[i + 1] : null;
|
|
42
|
+
}
|
|
43
|
+
const hasFlag = (f) => process.argv.includes('--' + f);
|
|
44
|
+
|
|
45
|
+
const projectDir = resolve(argVal('dir') || CWD);
|
|
46
|
+
const dbPathArg = argVal('db') ? resolve(argVal('db')) : join(projectDir, '.monomind', 'monograph.db');
|
|
47
|
+
const outputArg = argVal('output')? resolve(argVal('output')) : join(projectDir, '.understand', 'knowledge-graph.json');
|
|
48
|
+
const batchSize = parseInt(argVal('batch-size') || '5', 10);
|
|
49
|
+
const maxFiles = parseInt(argVal('max-files') || '0', 10);
|
|
50
|
+
const dryRun = hasFlag('dry-run');
|
|
51
|
+
const noLlm = hasFlag('no-llm');
|
|
52
|
+
const layersOnly = hasFlag('layers-only');
|
|
53
|
+
const incremental = hasFlag('incremental');
|
|
54
|
+
const onboard = hasFlag('onboard');
|
|
55
|
+
const onboardOut = argVal('onboard-out') ? resolve(argVal('onboard-out')) : join(projectDir, 'ONBOARDING.md');
|
|
56
|
+
|
|
57
|
+
// ── Resolve @monoes/monograph for DB access ──────────────────────────────────
|
|
58
|
+
function resolveMonograph() {
|
|
59
|
+
const req = createRequire(import.meta.url);
|
|
60
|
+
const candidates = [
|
|
61
|
+
// CLI package's own node_modules (npx / global npm install)
|
|
62
|
+
join(__dir, '..', 'node_modules', '@monoes', 'monograph'),
|
|
63
|
+
// Global npm / homebrew
|
|
64
|
+
(() => { try { return join(req.resolve('npm/bin/npm-cli.js'), '..', '..', '..', '@monoes', 'monograph'); } catch { return null; } })(),
|
|
65
|
+
// Monorepo root
|
|
66
|
+
join(__dir, '..', '..', '..', '..', 'node_modules', '@monoes', 'monograph'),
|
|
67
|
+
// User project
|
|
68
|
+
join(projectDir, 'node_modules', '@monoes', 'monograph'),
|
|
69
|
+
].filter(Boolean);
|
|
70
|
+
|
|
71
|
+
for (const c of candidates) {
|
|
72
|
+
try {
|
|
73
|
+
if (existsSync(c)) return req(c);
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
76
|
+
// Last resort: require by name (works when installed globally)
|
|
77
|
+
try { return req('@monoes/monograph'); } catch {}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Heuristic layer detection (ported from understand-anything layer-detector) ─
|
|
82
|
+
const LAYER_PATTERNS = [
|
|
83
|
+
{ patterns: ['routes', 'controller', 'handler', 'endpoint', 'api'], name: 'API Layer', description: 'HTTP endpoints, route handlers, and API controllers' },
|
|
84
|
+
{ patterns: ['service', 'usecase', 'use-case', 'business'], name: 'Service Layer', description: 'Business logic and application services' },
|
|
85
|
+
{ patterns: ['model', 'entity', 'schema', 'database', 'db', 'migration','repository', 'repo'],
|
|
86
|
+
name: 'Data Layer', description: 'Data models, database access, and persistence' },
|
|
87
|
+
{ patterns: ['component', 'view', 'page', 'screen', 'layout', 'widget', 'ui'],
|
|
88
|
+
name: 'UI Layer', description: 'User interface components and views' },
|
|
89
|
+
{ patterns: ['middleware', 'interceptor', 'guard', 'filter', 'pipe'], name: 'Middleware Layer', description: 'Request/response middleware and interceptors' },
|
|
90
|
+
{ patterns: ['client', 'integration', 'external', 'sdk', 'vendor', 'adapter'],
|
|
91
|
+
name: 'External Services', description: 'External service integrations, SDKs, and third-party adapters' },
|
|
92
|
+
{ patterns: ['worker', 'job', 'queue', 'cron', 'consumer', 'processor', 'scheduler', 'background'],
|
|
93
|
+
name: 'Background Tasks', description: 'Background workers, job processors, and scheduled tasks' },
|
|
94
|
+
{ patterns: ['util', 'helper', 'lib', 'common', 'shared'], name: 'Utility Layer', description: 'Shared utilities, helpers, and common libraries' },
|
|
95
|
+
{ patterns: ['test', 'spec', '__test__', '__spec__', '__tests__'], name: 'Test Layer', description: 'Test files and test utilities' },
|
|
96
|
+
{ patterns: ['config', 'setting', 'env'], name: 'Configuration Layer', description: 'Application configuration and environment settings' },
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
function matchFileToLayer(filePath) {
|
|
100
|
+
const norm = filePath.replace(/\\/g, '/').toLowerCase();
|
|
101
|
+
const segments = norm.split('/');
|
|
102
|
+
for (const { patterns, name } of LAYER_PATTERNS) {
|
|
103
|
+
for (const seg of segments) {
|
|
104
|
+
for (const p of patterns) {
|
|
105
|
+
if (seg === p || seg === p + 's') return name;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function toLayerId(name) {
|
|
113
|
+
return 'layer:' + name.toLowerCase().replace(/\s+/g, '-');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function detectLayersHeuristic(fileNodes) {
|
|
117
|
+
const map = new Map(); // layerName → nodeIds[]
|
|
118
|
+
for (const node of fileNodes) {
|
|
119
|
+
const layerName = (node.file_path && matchFileToLayer(node.file_path)) || 'Core';
|
|
120
|
+
if (!map.has(layerName)) map.set(layerName, []);
|
|
121
|
+
map.get(layerName).push(node.id);
|
|
122
|
+
}
|
|
123
|
+
const layers = [];
|
|
124
|
+
for (const [name, nodeIds] of map) {
|
|
125
|
+
const pattern = LAYER_PATTERNS.find(p => p.name === name);
|
|
126
|
+
layers.push({ id: toLayerId(name), name, description: pattern?.description ?? 'Core application files', nodeIds });
|
|
127
|
+
}
|
|
128
|
+
return layers;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Anthropic API helpers (raw fetch — no SDK needed) ────────────────────────
|
|
132
|
+
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
|
133
|
+
const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages';
|
|
134
|
+
const MODEL = 'claude-haiku-4-5-20251001'; // cheapest for bulk analysis
|
|
135
|
+
|
|
136
|
+
async function callClaude(systemPrompt, userPrompt, maxTokens = 1024) {
|
|
137
|
+
const body = JSON.stringify({
|
|
138
|
+
model: MODEL,
|
|
139
|
+
max_tokens: maxTokens,
|
|
140
|
+
system: systemPrompt,
|
|
141
|
+
messages: [{ role: 'user', content: userPrompt }],
|
|
142
|
+
});
|
|
143
|
+
const headers = {
|
|
144
|
+
'Content-Type': 'application/json',
|
|
145
|
+
'x-api-key': ANTHROPIC_API_KEY,
|
|
146
|
+
'anthropic-version': '2023-06-01',
|
|
147
|
+
};
|
|
148
|
+
let lastError;
|
|
149
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
150
|
+
try {
|
|
151
|
+
const resp = await fetch(ANTHROPIC_URL, { method: 'POST', headers, body });
|
|
152
|
+
if (resp.ok) {
|
|
153
|
+
const data = await resp.json();
|
|
154
|
+
return data.content?.[0]?.text ?? '';
|
|
155
|
+
}
|
|
156
|
+
// Retry on 429 (rate limit) and 5xx; fail fast on 4xx
|
|
157
|
+
if (resp.status === 429 || resp.status >= 500) {
|
|
158
|
+
const retryAfter = parseInt(resp.headers.get('retry-after') || '0', 10);
|
|
159
|
+
const backoff = retryAfter > 0 ? retryAfter * 1000 : Math.min(2 ** attempt * 1000, 8000);
|
|
160
|
+
await new Promise(r => setTimeout(r, backoff));
|
|
161
|
+
lastError = new Error(`Anthropic API ${resp.status} (attempt ${attempt + 1}/3)`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const text = await resp.text();
|
|
165
|
+
throw new Error(`Anthropic API ${resp.status}: ${text.slice(0, 200)}`);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
lastError = e;
|
|
168
|
+
if (attempt === 2) break;
|
|
169
|
+
await new Promise(r => setTimeout(r, Math.min(2 ** attempt * 1000, 4000)));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
throw lastError || new Error('Anthropic API failed after 3 attempts');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function parseJson(text) {
|
|
176
|
+
try {
|
|
177
|
+
const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
|
|
178
|
+
const src = fenceMatch ? fenceMatch[1] : text;
|
|
179
|
+
const objMatch = src.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
|
|
180
|
+
if (objMatch) return JSON.parse(objMatch[0]);
|
|
181
|
+
} catch {}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Per-file analysis prompt (ported from llm-analyzer.ts)
|
|
186
|
+
function buildFilePrompt(filePath, content, projectContext) {
|
|
187
|
+
const truncated = content.length > 6000 ? content.slice(0, 6000) + '\n... (truncated)' : content;
|
|
188
|
+
return `You are a code analysis assistant. Analyze the following source file and return a JSON object.
|
|
189
|
+
|
|
190
|
+
Project context: ${projectContext}
|
|
191
|
+
|
|
192
|
+
File: ${filePath}
|
|
193
|
+
|
|
194
|
+
\`\`\`
|
|
195
|
+
${truncated}
|
|
196
|
+
\`\`\`
|
|
197
|
+
|
|
198
|
+
Return a JSON object with exactly these fields:
|
|
199
|
+
- "fileSummary": A concise summary of what this file does (1-2 sentences).
|
|
200
|
+
- "tags": An array of 2-5 relevant tags (e.g., ["utility", "async", "api"]).
|
|
201
|
+
- "complexity": One of "simple", "moderate", or "complex".
|
|
202
|
+
- "functionSummaries": An object mapping each function/method name to a 1-sentence summary (top 5 only).
|
|
203
|
+
- "classSummaries": An object mapping each class name to a 1-sentence summary.
|
|
204
|
+
|
|
205
|
+
Respond ONLY with the JSON object, no additional text.`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Batch file analysis prompt (multiple files at once for efficiency)
|
|
209
|
+
function buildBatchPrompt(files, projectContext) {
|
|
210
|
+
const fileBlocks = files.map(({ path, content }) => {
|
|
211
|
+
const truncated = content.length > 2000 ? content.slice(0, 2000) + '\n...' : content;
|
|
212
|
+
return `### ${path}\n\`\`\`\n${truncated}\n\`\`\``;
|
|
213
|
+
}).join('\n\n');
|
|
214
|
+
|
|
215
|
+
return `You are a code analysis assistant. Analyze the following source files and return a JSON object.
|
|
216
|
+
|
|
217
|
+
Project context: ${projectContext}
|
|
218
|
+
|
|
219
|
+
${fileBlocks}
|
|
220
|
+
|
|
221
|
+
Return a JSON object where each key is the exact file path and the value is:
|
|
222
|
+
- "fileSummary": 1-2 sentence summary of what the file does.
|
|
223
|
+
- "tags": 2-5 relevant tags.
|
|
224
|
+
- "complexity": "simple", "moderate", or "complex".
|
|
225
|
+
- "functionSummaries": object of function name → 1-sentence summary (top 5 per file).
|
|
226
|
+
- "classSummaries": object of class name → 1-sentence summary.
|
|
227
|
+
|
|
228
|
+
Respond ONLY with the JSON object mapping file paths to their analysis.`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Layer detection prompt (ported from layer-detector.ts)
|
|
232
|
+
function buildLayerPrompt(filePaths) {
|
|
233
|
+
const list = filePaths.slice(0, 200).map(f => ` - ${f}`).join('\n');
|
|
234
|
+
return `You are a software architecture analyst. Given these file paths, identify 3-8 logical architectural layers.
|
|
235
|
+
|
|
236
|
+
${list}
|
|
237
|
+
|
|
238
|
+
Return a JSON array where each element has:
|
|
239
|
+
- "name": Short layer name (e.g., "API", "Data", "UI")
|
|
240
|
+
- "description": What this layer does (1 sentence)
|
|
241
|
+
- "filePatterns": Path prefixes that belong to this layer (e.g., ["src/routes/", "src/controllers/"])
|
|
242
|
+
|
|
243
|
+
Every file should belong to exactly one layer. Respond ONLY with the JSON array.`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── .understandignore support (ported from ignore-filter.ts) ────────────────
|
|
247
|
+
const DEFAULT_IGNORE_PATTERNS = [
|
|
248
|
+
'node_modules/', '.git/', 'vendor/', 'venv/', '.venv/', '__pycache__/',
|
|
249
|
+
'dist/', 'build/', 'out/', 'coverage/', '.next/', '.cache/', '.turbo/', 'target/', 'obj/',
|
|
250
|
+
'*.lock', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml',
|
|
251
|
+
'*.png', '*.jpg', '*.jpeg', '*.gif', '*.svg', '*.ico', '*.woff', '*.woff2',
|
|
252
|
+
'*.ttf', '*.eot', '*.mp3', '*.mp4', '*.pdf', '*.zip', '*.tar', '*.gz',
|
|
253
|
+
'*.min.js', '*.min.css', '*.map', '*.generated.*',
|
|
254
|
+
'.idea/', '.vscode/', '*.log',
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
function loadIgnorePatterns(dir) {
|
|
258
|
+
const patterns = [...DEFAULT_IGNORE_PATTERNS];
|
|
259
|
+
const locations = [
|
|
260
|
+
join(dir, '.understand-anything', '.understandignore'),
|
|
261
|
+
join(dir, '.understandignore'),
|
|
262
|
+
];
|
|
263
|
+
for (const p of locations) {
|
|
264
|
+
if (existsSync(p)) {
|
|
265
|
+
try {
|
|
266
|
+
const lines = readFileSync(p, 'utf-8').split('\n')
|
|
267
|
+
.map(l => l.trim()).filter(l => l && !l.startsWith('#'));
|
|
268
|
+
patterns.push(...lines);
|
|
269
|
+
} catch {}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return patterns;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function makeIgnoreMatcher(patterns) {
|
|
276
|
+
return function isIgnored(filePath) {
|
|
277
|
+
const norm = filePath.replace(/\\/g, '/');
|
|
278
|
+
for (const pat of patterns) {
|
|
279
|
+
if (pat.startsWith('!')) continue; // negation — skip for simplicity
|
|
280
|
+
if (pat.endsWith('/')) {
|
|
281
|
+
// directory pattern
|
|
282
|
+
if (norm.includes('/' + pat.slice(0, -1) + '/') || norm.startsWith(pat)) return true;
|
|
283
|
+
} else if (pat.startsWith('*.')) {
|
|
284
|
+
// extension glob
|
|
285
|
+
if (norm.endsWith(pat.slice(1))) return true;
|
|
286
|
+
} else if (pat.includes('*')) {
|
|
287
|
+
// simple wildcard — match anywhere in path
|
|
288
|
+
const re = new RegExp('^' + pat.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
|
|
289
|
+
if (re.test(norm) || re.test(norm.split('/').pop() || '')) return true;
|
|
290
|
+
} else {
|
|
291
|
+
// exact segment or prefix
|
|
292
|
+
if (norm === pat || norm.includes('/' + pat) || norm.startsWith(pat)) return true;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return false;
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const _ignorePatterns = loadIgnorePatterns(projectDir);
|
|
300
|
+
const isIgnoredByUser = makeIgnoreMatcher(_ignorePatterns);
|
|
301
|
+
|
|
302
|
+
// ── Incremental mode helpers (ported from staleness.ts) ──────────────────────
|
|
303
|
+
function getChangedFiles(dir, lastHash) {
|
|
304
|
+
const files = new Set();
|
|
305
|
+
function runGit(args) {
|
|
306
|
+
try {
|
|
307
|
+
const out = execFileSync('git', args, { cwd: dir, encoding: 'utf-8' });
|
|
308
|
+
for (const line of out.split('\n')) {
|
|
309
|
+
const trimmed = line.trim();
|
|
310
|
+
if (!trimmed) continue;
|
|
311
|
+
// `git status --porcelain` lines look like " M path/to/file" or "?? path"
|
|
312
|
+
// Strip the leading status code if present
|
|
313
|
+
const path = trimmed.length > 3 && trimmed[2] === ' ' ? trimmed.slice(3) : trimmed;
|
|
314
|
+
files.add(path);
|
|
315
|
+
}
|
|
316
|
+
} catch { /* git not available or no diff */ }
|
|
317
|
+
}
|
|
318
|
+
// Committed changes since last run
|
|
319
|
+
runGit(['diff', `${lastHash}..HEAD`, '--name-only']);
|
|
320
|
+
// Uncommitted working tree changes (staged + unstaged + untracked)
|
|
321
|
+
runGit(['status', '--porcelain']);
|
|
322
|
+
return [...files];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function getCurrentCommitHash(dir) {
|
|
326
|
+
try {
|
|
327
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: dir, encoding: 'utf-8' }).trim();
|
|
328
|
+
} catch {
|
|
329
|
+
return '';
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ── Language detection (slim port of language-registry.ts) ──────────────────
|
|
334
|
+
const LANGUAGE_BY_EXT = {
|
|
335
|
+
'.ts': 'TypeScript', '.tsx': 'TypeScript',
|
|
336
|
+
'.js': 'JavaScript', '.jsx': 'JavaScript', '.mjs': 'JavaScript', '.cjs': 'JavaScript',
|
|
337
|
+
'.py': 'Python', '.pyi': 'Python',
|
|
338
|
+
'.rs': 'Rust', '.go': 'Go',
|
|
339
|
+
'.java': 'Java', '.kt': 'Kotlin', '.scala': 'Scala',
|
|
340
|
+
'.rb': 'Ruby', '.php': 'PHP', '.swift': 'Swift',
|
|
341
|
+
'.c': 'C', '.h': 'C', '.cpp': 'C++', '.cc': 'C++', '.hpp': 'C++', '.hxx': 'C++',
|
|
342
|
+
'.cs': 'C#', '.fs': 'F#',
|
|
343
|
+
'.sh': 'Shell', '.bash': 'Shell', '.zsh': 'Shell',
|
|
344
|
+
'.lua': 'Lua', '.r': 'R', '.dart': 'Dart', '.ex': 'Elixir', '.exs': 'Elixir',
|
|
345
|
+
'.sol': 'Solidity', '.zig': 'Zig', '.nim': 'Nim',
|
|
346
|
+
'.html': 'HTML', '.css': 'CSS', '.scss': 'SCSS', '.vue': 'Vue', '.svelte': 'Svelte',
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
function detectLanguages(fileNodes) {
|
|
350
|
+
const counts = new Map();
|
|
351
|
+
for (const n of fileNodes) {
|
|
352
|
+
if (!n.file_path) continue;
|
|
353
|
+
const lastDot = n.file_path.lastIndexOf('.');
|
|
354
|
+
if (lastDot === -1) continue;
|
|
355
|
+
const ext = n.file_path.slice(lastDot).toLowerCase();
|
|
356
|
+
const lang = LANGUAGE_BY_EXT[ext];
|
|
357
|
+
if (lang) counts.set(lang, (counts.get(lang) || 0) + 1);
|
|
358
|
+
}
|
|
359
|
+
// Return languages with >=3 files, sorted by count desc
|
|
360
|
+
return [...counts.entries()]
|
|
361
|
+
.filter(([, c]) => c >= 3)
|
|
362
|
+
.sort((a, b) => b[1] - a[1])
|
|
363
|
+
.map(([lang]) => lang);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── Framework detection (slim port of framework-registry.ts) ────────────────
|
|
367
|
+
const FRAMEWORK_SIGNATURES = [
|
|
368
|
+
// [name, manifestFile, keywords[]]
|
|
369
|
+
['React', 'package.json', ['"react"']],
|
|
370
|
+
['Next.js', 'package.json', ['"next"']],
|
|
371
|
+
['Vue', 'package.json', ['"vue"']],
|
|
372
|
+
['Svelte', 'package.json', ['"svelte"']],
|
|
373
|
+
['Angular', 'package.json', ['"@angular/core"']],
|
|
374
|
+
['Express', 'package.json', ['"express"']],
|
|
375
|
+
['Fastify', 'package.json', ['"fastify"']],
|
|
376
|
+
['NestJS', 'package.json', ['"@nestjs/core"']],
|
|
377
|
+
['Vite', 'package.json', ['"vite"']],
|
|
378
|
+
['Webpack', 'package.json', ['"webpack"']],
|
|
379
|
+
['TypeScript', 'package.json', ['"typescript"']],
|
|
380
|
+
['Anthropic SDK','package.json', ['"@anthropic-ai/sdk"']],
|
|
381
|
+
['Django', 'requirements.txt', ['django']],
|
|
382
|
+
['Flask', 'requirements.txt', ['flask']],
|
|
383
|
+
['FastAPI', 'requirements.txt', ['fastapi']],
|
|
384
|
+
['Rails', 'Gemfile', ['rails']],
|
|
385
|
+
['Spring Boot', 'pom.xml', ['spring-boot']],
|
|
386
|
+
['Axum', 'Cargo.toml', ['axum']],
|
|
387
|
+
['Actix', 'Cargo.toml', ['actix-web']],
|
|
388
|
+
['Tokio', 'Cargo.toml', ['tokio']],
|
|
389
|
+
['Gin', 'go.mod', ['gin-gonic/gin']],
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
function findManifests(dir, manifestName, maxDepth = 4) {
|
|
393
|
+
const fs = createRequire(import.meta.url)('node:fs');
|
|
394
|
+
const results = [];
|
|
395
|
+
function walk(d, depth) {
|
|
396
|
+
if (depth > maxDepth) return;
|
|
397
|
+
let entries;
|
|
398
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); }
|
|
399
|
+
catch { return; }
|
|
400
|
+
for (const e of entries) {
|
|
401
|
+
if (e.name === 'node_modules' || e.name === 'dist' || e.name.startsWith('.')) continue;
|
|
402
|
+
const full = join(d, e.name);
|
|
403
|
+
if (e.isDirectory()) walk(full, depth + 1);
|
|
404
|
+
else if (e.name === manifestName) results.push(full);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
walk(dir, 0);
|
|
408
|
+
return results;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function detectFrameworks(dir) {
|
|
412
|
+
const detected = [];
|
|
413
|
+
const seen = new Set();
|
|
414
|
+
// Cache manifest contents by name to avoid re-reading
|
|
415
|
+
const manifestCache = new Map();
|
|
416
|
+
for (const [name, manifest, keywords] of FRAMEWORK_SIGNATURES) {
|
|
417
|
+
if (seen.has(name)) continue;
|
|
418
|
+
let manifestPaths = manifestCache.get(manifest);
|
|
419
|
+
if (!manifestPaths) {
|
|
420
|
+
manifestPaths = findManifests(dir, manifest);
|
|
421
|
+
manifestCache.set(manifest, manifestPaths);
|
|
422
|
+
}
|
|
423
|
+
for (const p of manifestPaths) {
|
|
424
|
+
try {
|
|
425
|
+
const content = readFileSync(p, 'utf-8').toLowerCase();
|
|
426
|
+
if (keywords.some(k => content.includes(k.toLowerCase()))) {
|
|
427
|
+
detected.push(name);
|
|
428
|
+
seen.add(name);
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
} catch {}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return detected;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ── Onboarding guide builder (ported from onboard-builder.ts) ───────────────
|
|
438
|
+
function buildOnboardingGuide(graphJson) {
|
|
439
|
+
const { project, nodes, layers, tour = [] } = graphJson;
|
|
440
|
+
const lines = [];
|
|
441
|
+
|
|
442
|
+
lines.push(`# ${project.name}`);
|
|
443
|
+
lines.push('');
|
|
444
|
+
if (project.description) {
|
|
445
|
+
lines.push(`> ${project.description}`);
|
|
446
|
+
lines.push('');
|
|
447
|
+
}
|
|
448
|
+
lines.push('| | |');
|
|
449
|
+
lines.push('|---|---|');
|
|
450
|
+
if (project.languages?.length) lines.push(`| **Languages** | ${project.languages.join(', ')} |`);
|
|
451
|
+
if (project.frameworks?.length) lines.push(`| **Frameworks** | ${project.frameworks.join(', ')} |`);
|
|
452
|
+
lines.push(`| **Components** | ${nodes.length} nodes |`);
|
|
453
|
+
lines.push(`| **Last Analyzed** | ${project.analyzedAt} |`);
|
|
454
|
+
lines.push('');
|
|
455
|
+
|
|
456
|
+
if (layers.length > 0) {
|
|
457
|
+
lines.push('## Architecture');
|
|
458
|
+
lines.push('');
|
|
459
|
+
lines.push('The project is organized into the following layers:');
|
|
460
|
+
lines.push('');
|
|
461
|
+
const nodeMap = new Map(nodes.map(n => [n.id, n]));
|
|
462
|
+
for (const layer of layers) {
|
|
463
|
+
lines.push(`### ${layer.name}`);
|
|
464
|
+
lines.push('');
|
|
465
|
+
if (layer.description) { lines.push(layer.description); lines.push(''); }
|
|
466
|
+
const members = (layer.nodeIds || []).map(id => nodeMap.get(id)?.name).filter(Boolean);
|
|
467
|
+
if (members.length > 0) { lines.push(`Key components: ${members.slice(0, 8).join(', ')}`); lines.push(''); }
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (tour.length > 0) {
|
|
472
|
+
lines.push('## Getting Started');
|
|
473
|
+
lines.push('');
|
|
474
|
+
lines.push('Follow this guided tour to understand the codebase:');
|
|
475
|
+
lines.push('');
|
|
476
|
+
const nodeMap = new Map(nodes.map(n => [n.id, n]));
|
|
477
|
+
for (const step of tour) {
|
|
478
|
+
lines.push(`### ${step.order}. ${step.title}`);
|
|
479
|
+
lines.push('');
|
|
480
|
+
lines.push(step.description);
|
|
481
|
+
lines.push('');
|
|
482
|
+
const stepNodes = (step.nodeIds || []).map(id => nodeMap.get(id)).filter(Boolean);
|
|
483
|
+
if (stepNodes.length > 0) {
|
|
484
|
+
lines.push('**Files to look at:**');
|
|
485
|
+
for (const node of stepNodes) {
|
|
486
|
+
if (node.filePath) lines.push(`- \`${node.filePath}\` — ${node.summary}`);
|
|
487
|
+
}
|
|
488
|
+
lines.push('');
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const FILE_MAP_LIMIT = 50;
|
|
494
|
+
const HOTSPOT_LIMIT = 20;
|
|
495
|
+
|
|
496
|
+
const fileNodes = nodes.filter(n => n.type === 'file' && n.filePath && n.summary);
|
|
497
|
+
if (fileNodes.length > 0) {
|
|
498
|
+
lines.push('## File Map');
|
|
499
|
+
lines.push('');
|
|
500
|
+
if (fileNodes.length > FILE_MAP_LIMIT) {
|
|
501
|
+
lines.push(`Showing ${FILE_MAP_LIMIT} of ${fileNodes.length} analyzed files. See \`.understand/knowledge-graph.json\` for the full list.`);
|
|
502
|
+
lines.push('');
|
|
503
|
+
}
|
|
504
|
+
lines.push('| File | Purpose | Complexity |');
|
|
505
|
+
lines.push('|------|---------|------------|');
|
|
506
|
+
for (const node of fileNodes.slice(0, FILE_MAP_LIMIT)) {
|
|
507
|
+
const summary = (node.summary || '').replace(/\|/g, '\\|');
|
|
508
|
+
lines.push(`| \`${node.filePath}\` | ${summary} | ${node.complexity || 'moderate'} |`);
|
|
509
|
+
}
|
|
510
|
+
lines.push('');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const complexNodes = nodes.filter(n => n.complexity === 'complex');
|
|
514
|
+
if (complexNodes.length > 0) {
|
|
515
|
+
lines.push('## Complexity Hotspots');
|
|
516
|
+
lines.push('');
|
|
517
|
+
lines.push('These components are the most complex and deserve extra attention:');
|
|
518
|
+
if (complexNodes.length > HOTSPOT_LIMIT) {
|
|
519
|
+
lines.push('');
|
|
520
|
+
lines.push(`Showing top ${HOTSPOT_LIMIT} of ${complexNodes.length} complex components.`);
|
|
521
|
+
}
|
|
522
|
+
lines.push('');
|
|
523
|
+
for (const node of complexNodes.slice(0, HOTSPOT_LIMIT)) {
|
|
524
|
+
lines.push(`- **${node.name}** (${node.type}): ${node.summary || ''}`);
|
|
525
|
+
}
|
|
526
|
+
lines.push('');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
lines.push('---');
|
|
530
|
+
lines.push('');
|
|
531
|
+
lines.push(`*Generated by [monomind](https://github.com/nokhodian/monomind) from knowledge graph v${graphJson.version}*`);
|
|
532
|
+
lines.push('');
|
|
533
|
+
|
|
534
|
+
return lines.join('\n');
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ── File reading with graceful skip ─────────────────────────────────────────
|
|
538
|
+
function readFileSafe(absPath) {
|
|
539
|
+
try {
|
|
540
|
+
const content = readFileSync(absPath, 'utf-8');
|
|
541
|
+
return content;
|
|
542
|
+
} catch {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const SKIP_EXTENSIONS = new Set([
|
|
548
|
+
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.webp',
|
|
549
|
+
'.pdf', '.zip', '.tar', '.gz', '.wasm', '.map',
|
|
550
|
+
'.lock', '.lockb', '.db', '.sqlite', '.bin', '.exe',
|
|
551
|
+
]);
|
|
552
|
+
function shouldAnalyze(filePath) {
|
|
553
|
+
if (!filePath) return false;
|
|
554
|
+
const ext = '.' + filePath.split('.').pop()?.toLowerCase();
|
|
555
|
+
if (SKIP_EXTENSIONS.has(ext)) return false;
|
|
556
|
+
if (isIgnoredByUser(filePath)) return false;
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
561
|
+
async function main() {
|
|
562
|
+
console.log('[understand] Starting semantic enrichment for', projectDir);
|
|
563
|
+
|
|
564
|
+
if (!existsSync(dbPathArg)) {
|
|
565
|
+
console.error('[understand] monograph.db not found at', dbPathArg);
|
|
566
|
+
console.error('[understand] Build the graph first: npx monomind monograph build');
|
|
567
|
+
process.exit(1);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const mg = resolveMonograph();
|
|
571
|
+
if (!mg) {
|
|
572
|
+
console.error('[understand] Cannot find @monoes/monograph. Run: npm install @monoes/monograph');
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Retry openDb against SQLite BUSY when monograph is building in the background
|
|
577
|
+
let db;
|
|
578
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
579
|
+
try { db = mg.openDb(dbPathArg); break; }
|
|
580
|
+
catch (e) {
|
|
581
|
+
if (attempt === 4) throw e;
|
|
582
|
+
const msg = String(e?.message || e);
|
|
583
|
+
if (!/busy|locked/i.test(msg)) throw e;
|
|
584
|
+
console.log(`[understand] monograph.db busy, retrying in ${(attempt + 1) * 2}s...`);
|
|
585
|
+
await new Promise(r => setTimeout(r, (attempt + 1) * 2000));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Ensure properties column exists
|
|
590
|
+
try { db.prepare(`ALTER TABLE nodes ADD COLUMN properties TEXT`).run(); } catch {}
|
|
591
|
+
try { db.prepare(`CREATE TABLE IF NOT EXISTS communities (id INTEGER PRIMARY KEY, label TEXT, size INTEGER NOT NULL DEFAULT 0, cohesion_score REAL NOT NULL DEFAULT 0.0)`).run(); } catch {}
|
|
592
|
+
|
|
593
|
+
// ── Load all file nodes ──────────────────────────────────────────────────
|
|
594
|
+
let fileNodes = db.prepare(`SELECT id, name, file_path, properties FROM nodes WHERE label = 'File' AND file_path IS NOT NULL`).all();
|
|
595
|
+
console.log(`[understand] Found ${fileNodes.length} file nodes in DB`);
|
|
596
|
+
|
|
597
|
+
// ── Layers-only mode ─────────────────────────────────────────────────────
|
|
598
|
+
if (layersOnly) {
|
|
599
|
+
console.log('[understand] Layers-only mode — skipping per-file analysis');
|
|
600
|
+
await detectAndWriteLayers(db, fileNodes, noLlm, dryRun);
|
|
601
|
+
mg.closeDb(db);
|
|
602
|
+
console.log('[understand] Done (layers-only)');
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ── Incremental mode: only re-analyze changed files ─────────────────────
|
|
607
|
+
let changedFileSet = null; // null means "analyze all"
|
|
608
|
+
if (incremental) {
|
|
609
|
+
let lastHash = '';
|
|
610
|
+
try {
|
|
611
|
+
const row = db.prepare(`SELECT value FROM index_meta WHERE key = 'ua_last_commit'`).get();
|
|
612
|
+
lastHash = row?.value || '';
|
|
613
|
+
} catch {}
|
|
614
|
+
if (lastHash) {
|
|
615
|
+
const changed = getChangedFiles(projectDir, lastHash);
|
|
616
|
+
if (changed.length > 0) {
|
|
617
|
+
changedFileSet = new Set(changed);
|
|
618
|
+
console.log(`[understand] Incremental mode: ${changed.length} files changed since ${lastHash.slice(0, 8)}`);
|
|
619
|
+
} else {
|
|
620
|
+
console.log('[understand] Incremental mode: no changes detected since last run — skipping analysis');
|
|
621
|
+
mg.closeDb(db);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
} else {
|
|
625
|
+
console.log('[understand] Incremental mode: no previous commit hash found — running full analysis');
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ── Filter files that need analysis ─────────────────────────────────────
|
|
630
|
+
const toAnalyze = fileNodes.filter(n => {
|
|
631
|
+
if (!shouldAnalyze(n.file_path)) return false;
|
|
632
|
+
if (changedFileSet) {
|
|
633
|
+
// Match by relative or absolute path
|
|
634
|
+
const rel = n.file_path.startsWith('/') ? relative(projectDir, n.file_path) : n.file_path;
|
|
635
|
+
return changedFileSet.has(rel) || changedFileSet.has(n.file_path);
|
|
636
|
+
}
|
|
637
|
+
return true;
|
|
638
|
+
});
|
|
639
|
+
const limit = maxFiles > 0 ? Math.min(maxFiles, toAnalyze.length) : toAnalyze.length;
|
|
640
|
+
const batch = toAnalyze.slice(0, limit);
|
|
641
|
+
console.log(`[understand] Analyzing ${batch.length} files (${toAnalyze.length - batch.length} skipped/already enriched)`);
|
|
642
|
+
|
|
643
|
+
if (!ANTHROPIC_API_KEY && !noLlm) {
|
|
644
|
+
console.warn('[understand] ANTHROPIC_API_KEY not set — falling back to --no-llm heuristic mode');
|
|
645
|
+
// Fall through to heuristic only
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const useLlm = !noLlm && !!ANTHROPIC_API_KEY;
|
|
649
|
+
|
|
650
|
+
// ── Get project context for better prompts ────────────────────────────────
|
|
651
|
+
let projectContext = `Project directory: ${basename(projectDir)}`;
|
|
652
|
+
try {
|
|
653
|
+
const pkgPath = join(projectDir, 'package.json');
|
|
654
|
+
if (existsSync(pkgPath)) {
|
|
655
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
656
|
+
projectContext = `${pkg.name || basename(projectDir)}: ${pkg.description || ''}`.trim();
|
|
657
|
+
}
|
|
658
|
+
} catch {}
|
|
659
|
+
|
|
660
|
+
// ── Per-file LLM analysis ────────────────────────────────────────────────
|
|
661
|
+
const analysisMap = {}; // file_path → { fileSummary, tags, complexity, functionSummaries, classSummaries }
|
|
662
|
+
|
|
663
|
+
if (useLlm && batch.length > 0) {
|
|
664
|
+
const system = 'You are an expert code analysis assistant. Always respond with valid JSON only.';
|
|
665
|
+
const chunks = [];
|
|
666
|
+
for (let i = 0; i < batch.length; i += batchSize) chunks.push(batch.slice(i, i + batchSize));
|
|
667
|
+
|
|
668
|
+
let done = 0;
|
|
669
|
+
for (const chunk of chunks) {
|
|
670
|
+
// Read file contents
|
|
671
|
+
const files = chunk
|
|
672
|
+
.map(n => {
|
|
673
|
+
const absPath = n.file_path.startsWith('/') ? n.file_path : join(projectDir, n.file_path);
|
|
674
|
+
const content = readFileSafe(absPath);
|
|
675
|
+
return content ? { path: n.file_path, content, nodeId: n.id } : null;
|
|
676
|
+
})
|
|
677
|
+
.filter(Boolean);
|
|
678
|
+
|
|
679
|
+
if (files.length === 0) { done += chunk.length; continue; }
|
|
680
|
+
|
|
681
|
+
process.stdout.write(`\r[understand] Analyzing files ${done + 1}–${Math.min(done + files.length, batch.length)} / ${batch.length}...`);
|
|
682
|
+
|
|
683
|
+
try {
|
|
684
|
+
let text;
|
|
685
|
+
if (files.length === 1) {
|
|
686
|
+
text = await callClaude(system, buildFilePrompt(files[0].path, files[0].content, projectContext));
|
|
687
|
+
const parsed = parseJson(text);
|
|
688
|
+
if (parsed) analysisMap[files[0].path] = parsed;
|
|
689
|
+
} else {
|
|
690
|
+
text = await callClaude(system, buildBatchPrompt(files, projectContext), 2048);
|
|
691
|
+
const parsed = parseJson(text);
|
|
692
|
+
if (parsed && typeof parsed === 'object') {
|
|
693
|
+
for (const [fp, analysis] of Object.entries(parsed)) {
|
|
694
|
+
if (analysis && typeof analysis === 'object') analysisMap[fp] = analysis;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
} catch (e) {
|
|
699
|
+
console.warn(`\n[understand] API error for batch at ${done}: ${e.message}`);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
done += files.length;
|
|
703
|
+
// Polite rate limiting
|
|
704
|
+
if (chunks.indexOf(chunk) < chunks.length - 1) await new Promise(r => setTimeout(r, 300));
|
|
705
|
+
}
|
|
706
|
+
console.log(`\n[understand] LLM analysis complete: ${Object.keys(analysisMap).length} files analyzed`);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// ── Write analysis back to DB ─────────────────────────────────────────────
|
|
710
|
+
const updateNode = db.prepare(`UPDATE nodes SET properties = ? WHERE id = ?`);
|
|
711
|
+
let written = 0;
|
|
712
|
+
|
|
713
|
+
if (!dryRun) {
|
|
714
|
+
const tx = db.transaction(() => {
|
|
715
|
+
for (const node of batch) {
|
|
716
|
+
const existing = node.properties ? (() => { try { return JSON.parse(node.properties); } catch { return {}; } })() : {};
|
|
717
|
+
const llmData = analysisMap[node.file_path] || {};
|
|
718
|
+
const merged = {
|
|
719
|
+
...existing,
|
|
720
|
+
...(llmData.fileSummary ? { summary: llmData.fileSummary } : {}),
|
|
721
|
+
...(llmData.tags ? { tags: llmData.tags } : {}),
|
|
722
|
+
...(llmData.complexity ? { complexity: llmData.complexity } : {}),
|
|
723
|
+
...(llmData.languageNotes ? { languageNotes: llmData.languageNotes } : {}),
|
|
724
|
+
...(llmData.functionSummaries ? { functionSummaries: llmData.functionSummaries } : {}),
|
|
725
|
+
...(llmData.classSummaries ? { classSummaries: llmData.classSummaries } : {}),
|
|
726
|
+
ua_analyzed_at: new Date().toISOString(),
|
|
727
|
+
};
|
|
728
|
+
updateNode.run(JSON.stringify(merged), node.id);
|
|
729
|
+
written++;
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
tx();
|
|
733
|
+
console.log(`[understand] Wrote enrichment data to ${written} nodes`);
|
|
734
|
+
} else {
|
|
735
|
+
console.log(`[understand] DRY RUN — would update ${batch.length} nodes`);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// ── Layer detection ──────────────────────────────────────────────────────
|
|
739
|
+
const layers = await detectAndWriteLayers(db, fileNodes, noLlm || !useLlm, dryRun, projectDir);
|
|
740
|
+
|
|
741
|
+
// ── Emit graph.json (for compatibility with ua-import.mjs) ──────────────
|
|
742
|
+
const graphJson = buildGraphJson(projectDir, fileNodes, analysisMap, layers);
|
|
743
|
+
const outputDir = dirname(outputArg);
|
|
744
|
+
if (!dryRun) {
|
|
745
|
+
mkdirSync(outputDir, { recursive: true });
|
|
746
|
+
writeFileSync(outputArg, JSON.stringify(graphJson, null, 2), 'utf-8');
|
|
747
|
+
console.log(`[understand] graph.json written to ${outputArg}`);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// ── Rebuild FTS ──────────────────────────────────────────────────────────
|
|
751
|
+
if (!dryRun) {
|
|
752
|
+
try {
|
|
753
|
+
db.prepare(`INSERT INTO nodes_fts(nodes_fts) VALUES('rebuild')`).run();
|
|
754
|
+
console.log('[understand] FTS index rebuilt');
|
|
755
|
+
} catch {}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// ── Update index_meta ────────────────────────────────────────────────────
|
|
759
|
+
if (!dryRun) {
|
|
760
|
+
const upsertMeta = db.prepare(
|
|
761
|
+
`INSERT INTO index_meta (key, value) VALUES (?, ?)
|
|
762
|
+
ON CONFLICT(key) DO UPDATE SET value=excluded.value`
|
|
763
|
+
);
|
|
764
|
+
upsertMeta.run('ua_analyzed_at', new Date().toISOString());
|
|
765
|
+
const currentHash = getCurrentCommitHash(projectDir);
|
|
766
|
+
if (currentHash) upsertMeta.run('ua_last_commit', currentHash);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
mg.closeDb(db);
|
|
770
|
+
|
|
771
|
+
// ── Onboarding guide ─────────────────────────────────────────────────────
|
|
772
|
+
let onboardWritten = false;
|
|
773
|
+
if (onboard && !dryRun) {
|
|
774
|
+
const guide = buildOnboardingGuide(graphJson);
|
|
775
|
+
writeFileSync(onboardOut, guide, 'utf-8');
|
|
776
|
+
onboardWritten = true;
|
|
777
|
+
console.log(`[understand] Onboarding guide written to ${relative(CWD, onboardOut)}`);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// ── Final report ─────────────────────────────────────────────────────────
|
|
781
|
+
const title = dryRun
|
|
782
|
+
? '║ /monomind:understand — DRY RUN (no writes) ║'
|
|
783
|
+
: '║ /monomind:understand — Enrichment Complete ║';
|
|
784
|
+
console.log('\n╔══════════════════════════════════════════════════╗');
|
|
785
|
+
console.log(title);
|
|
786
|
+
console.log('╠══════════════════════════════════════════════════╣');
|
|
787
|
+
console.log(`║ DB: ${relative(CWD, dbPathArg).padEnd(31)}║`);
|
|
788
|
+
console.log(`║ Nodes enriched: ${String(written).padEnd(31)}║`);
|
|
789
|
+
console.log(`║ Communities: ${String(layers.length).padEnd(31)}║`);
|
|
790
|
+
console.log(`║ graph.json: ${relative(CWD, outputArg).padEnd(31)}║`);
|
|
791
|
+
if (onboardWritten) {
|
|
792
|
+
console.log(`║ ONBOARDING.md: ${relative(CWD, onboardOut).padEnd(31)}║`);
|
|
793
|
+
}
|
|
794
|
+
console.log('╚══════════════════════════════════════════════════╝');
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// ── Detect layers and write communities to DB ────────────────────────────────
|
|
798
|
+
async function detectAndWriteLayers(db, fileNodes, forceHeuristic, dryRun, dir) {
|
|
799
|
+
let layers;
|
|
800
|
+
|
|
801
|
+
if (!forceHeuristic && ANTHROPIC_API_KEY) {
|
|
802
|
+
console.log('[understand] Detecting architectural layers via LLM...');
|
|
803
|
+
const filePaths = fileNodes.map(n => n.file_path).filter(Boolean);
|
|
804
|
+
try {
|
|
805
|
+
const system = 'You are a software architecture expert. Respond with valid JSON only.';
|
|
806
|
+
const text = await callClaude(system, buildLayerPrompt(filePaths), 1024);
|
|
807
|
+
const parsed = parseJson(text);
|
|
808
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
809
|
+
layers = applyLlmLayers(fileNodes, parsed);
|
|
810
|
+
console.log(`[understand] LLM detected ${layers.length} layers`);
|
|
811
|
+
}
|
|
812
|
+
} catch (e) {
|
|
813
|
+
console.warn('[understand] LLM layer detection failed, falling back to heuristic:', e.message);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (!layers) {
|
|
818
|
+
layers = detectLayersHeuristic(fileNodes);
|
|
819
|
+
console.log(`[understand] Heuristic layer detection: ${layers.length} layers`);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (!dryRun) {
|
|
823
|
+
let communityIdx = 1000;
|
|
824
|
+
const upsertCommunity = db.prepare(
|
|
825
|
+
`INSERT INTO communities (id, label, size, cohesion_score)
|
|
826
|
+
VALUES (?, ?, ?, 0.8)
|
|
827
|
+
ON CONFLICT(id) DO UPDATE SET label=excluded.label, size=excluded.size`
|
|
828
|
+
);
|
|
829
|
+
const updateNodeCommunity = db.prepare(`UPDATE nodes SET community_id = ? WHERE id = ?`);
|
|
830
|
+
|
|
831
|
+
const tx = db.transaction(() => {
|
|
832
|
+
for (const layer of layers) {
|
|
833
|
+
upsertCommunity.run(communityIdx, layer.name, layer.nodeIds.length);
|
|
834
|
+
for (const nodeId of layer.nodeIds) {
|
|
835
|
+
updateNodeCommunity.run(communityIdx, nodeId);
|
|
836
|
+
}
|
|
837
|
+
communityIdx++;
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
tx();
|
|
841
|
+
console.log(`[understand] Wrote ${layers.length} communities to DB`);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return layers;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Apply LLM-suggested layer patterns to file nodes (ported from applyLLMLayers)
|
|
848
|
+
function applyLlmLayers(fileNodes, llmLayers) {
|
|
849
|
+
const map = new Map();
|
|
850
|
+
for (const l of llmLayers) map.set(l.name, []);
|
|
851
|
+
|
|
852
|
+
for (const node of fileNodes) {
|
|
853
|
+
if (!node.file_path) {
|
|
854
|
+
const other = map.get('Other') ?? [];
|
|
855
|
+
other.push(node.id);
|
|
856
|
+
map.set('Other', other);
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
const norm = node.file_path.replace(/\\/g, '/');
|
|
860
|
+
let assigned = false;
|
|
861
|
+
for (const l of llmLayers) {
|
|
862
|
+
for (const pattern of (l.filePatterns || [])) {
|
|
863
|
+
if (norm.startsWith(pattern) || norm.includes('/' + pattern)) {
|
|
864
|
+
map.get(l.name).push(node.id);
|
|
865
|
+
assigned = true;
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
if (assigned) break;
|
|
870
|
+
}
|
|
871
|
+
if (!assigned) {
|
|
872
|
+
const other = map.get('Other') ?? [];
|
|
873
|
+
other.push(node.id);
|
|
874
|
+
map.set('Other', other);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const layers = [];
|
|
879
|
+
for (const [name, nodeIds] of map) {
|
|
880
|
+
if (nodeIds.length === 0) continue;
|
|
881
|
+
const l = llmLayers.find(x => x.name === name);
|
|
882
|
+
layers.push({ id: toLayerId(name), name, description: l?.description ?? 'Uncategorized', nodeIds });
|
|
883
|
+
}
|
|
884
|
+
return layers;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Build a KnowledgeGraph-compatible graph.json for ua-import compatibility
|
|
888
|
+
function buildGraphJson(dir, fileNodes, analysisMap, layers) {
|
|
889
|
+
const projectName = basename(dir);
|
|
890
|
+
const nodes = fileNodes.map(n => {
|
|
891
|
+
const a = analysisMap[n.file_path] || {};
|
|
892
|
+
return {
|
|
893
|
+
id: 'file:' + (n.file_path || n.name),
|
|
894
|
+
type: 'file',
|
|
895
|
+
name: n.name,
|
|
896
|
+
filePath: n.file_path,
|
|
897
|
+
summary: a.fileSummary || '',
|
|
898
|
+
tags: a.tags || [],
|
|
899
|
+
complexity: a.complexity || 'moderate',
|
|
900
|
+
};
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
// Project metadata
|
|
904
|
+
let description = '';
|
|
905
|
+
try {
|
|
906
|
+
const pkgPath = join(dir, 'package.json');
|
|
907
|
+
if (existsSync(pkgPath)) {
|
|
908
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
909
|
+
description = pkg.description || '';
|
|
910
|
+
}
|
|
911
|
+
} catch {}
|
|
912
|
+
|
|
913
|
+
return {
|
|
914
|
+
version: '2.7.0',
|
|
915
|
+
kind: 'codebase',
|
|
916
|
+
project: {
|
|
917
|
+
name: projectName,
|
|
918
|
+
languages: detectLanguages(fileNodes),
|
|
919
|
+
frameworks: detectFrameworks(dir),
|
|
920
|
+
description,
|
|
921
|
+
analyzedAt: new Date().toISOString(),
|
|
922
|
+
gitCommitHash: getCurrentCommitHash(dir),
|
|
923
|
+
},
|
|
924
|
+
nodes,
|
|
925
|
+
edges: [],
|
|
926
|
+
layers: layers.map(l => ({ id: l.id, name: l.name, description: l.description, nodeIds: l.nodeIds })),
|
|
927
|
+
tour: [],
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
main().catch(e => {
|
|
932
|
+
console.error('[understand] Fatal error:', e.message);
|
|
933
|
+
process.exit(1);
|
|
934
|
+
});
|