Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,29 @@ The plugin discovers supported source files, skips common dependency and build d
### Run from the CLI

### 2. Create Configuration File
Create a config file (`configs/*.mjs`) mapping source files and project-specific categorization.
Prefer a data-only JSON config for shared or untrusted analysis settings.

```json
{
"name": "spring-demo-analysis",
"srcDir": "/path/to/spring-project",
"outDir": "/path/to/output_dir",
"sourceFiles": [
"src/main/java/**/*.java"
],
"perfData": {},
"roleRules": {
"Controller": "web_endpoint",
"Repository": "data_access"
}
}
```

`roleRules` assigns roles when a symbol name contains a key (case-insensitive).

Executable JavaScript configs (`configs/*.mjs`) are still supported for trusted
local workflows that need custom `guessRole` logic. They run arbitrary local
code, so only use them from sources you trust.

```javascript
// configs/spring_analysis.mjs
Expand Down Expand Up @@ -160,7 +182,13 @@ export default {
Execute Quarkify with your config file as the argument:

```bash
node quarkify.mjs configs/spring_analysis.mjs
node quarkify.mjs configs/spring_analysis.json
```

For trusted executable configs, opt in explicitly:

```bash
node quarkify.mjs --allow-executable-config configs/spring_analysis.mjs
```

---
Expand Down
32 changes: 30 additions & 2 deletions README_KR.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,29 @@ Quarkify는 대규모 상용 및 오픈소스 프로젝트에 적용하여 대
* [Node.js](https://nodejs.org/) v22.12.0 이상

### 2. 설정 파일(Config) 작성
분석할 타겟 프로젝트 경로와 소스 파일들을 설정하는 Config 파일(`configs/*.mjs`)을 작성합니다.
공유되거나 신뢰 여부가 불분명한 설정은 실행 코드가 없는 JSON config를 권장합니다.

```json
{
"name": "spring-demo-analysis",
"srcDir": "/path/to/spring-project",
"outDir": "/path/to/output_dir",
"sourceFiles": [
"src/main/java/**/*.java"
],
"perfData": {},
"roleRules": {
"Controller": "web_endpoint",
"Repository": "data_access"
}
}
```

`roleRules`는 심볼 이름에 키가 포함될 때 역할을 지정합니다(대소문자 무시).

커스텀 `guessRole` 로직이 필요한 신뢰된 로컬 workflow에서는 실행 가능한
JavaScript config(`configs/*.mjs`)도 사용할 수 있습니다. 이 파일은 로컬 코드를
실행할 수 있으므로 신뢰하는 출처의 설정에만 사용하세요.

```javascript
// configs/spring_analysis.mjs
Expand Down Expand Up @@ -125,7 +147,13 @@ export default {

### 3. 실행하기
```bash
node quarkify.mjs configs/spring_analysis.mjs
node quarkify.mjs configs/spring_analysis.json
```

신뢰된 실행형 config는 명시적으로 허용합니다.

```bash
node quarkify.mjs --allow-executable-config configs/spring_analysis.mjs
```

---
Expand Down
92 changes: 74 additions & 18 deletions quarkify.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@
* SOURCE_FILES / PERF_DATA / role classifier)를 외부 config 파일로 분리. (Preserving all decomposition logic from v3.1, while separating project-specific information (SRC_DIR / OUTPUT_DIR / SOURCE_FILES / PERF_DATA / role classifier) into external config files.)
*
* 사용법 (Usage):
* node quarkify_v7.mjs configs/sovereign_cuda.mjs
* node quarkify_v7.mjs configs/sovereign_metal.mjs
* node quarkify_v7.mjs configs/llama_cpp_cuda.mjs
* node quarkify.mjs configs/project.json
* node quarkify.mjs --allow-executable-config configs/trusted_project.mjs
*
* Config 인터페이스 (Config Interface) (configs/*.mjs):
* export default {
Expand All @@ -39,10 +38,11 @@ import { pathToFileURL } from 'url';
import { execSync } from 'child_process';

// ─── CLI / 컨피그 로드 (Load CLI / Config) ───
const configPath = process.argv[2];
const cli = parseCliArgs(process.argv.slice(2));
const configPath = cli.configPath;
if (!configPath) {
console.error('❌ 에러: 설정 파일 경로가 제공되지 않았습니다.');
console.error('사용법: node quarkify.mjs <configs/config_name.mjs>');
console.error('사용법: node quarkify.mjs [--allow-executable-config] <configs/config_name.json|mjs>');
process.exit(1);
}
if (!fs.existsSync(configPath)) {
Expand All @@ -57,27 +57,80 @@ if (!fs.existsSync(cfgAbs)) {

let CONFIG;
try {
const imported = await import(pathToFileURL(cfgAbs).href);
if (!imported || !imported.default) {
console.error(`❌ 에러: 설정 파일에 'default export'가 정의되어 있지 않습니다: "${configPath}"`);
process.exit(1);
}
CONFIG = imported.default;
CONFIG = validateConfig(await loadConfig(cfgAbs, cli.allowExecutableConfig));
} catch (err) {
console.error(`❌ 에러: 설정 파일을 불러오는 중 오류가 발생했습니다:`, err.message);
process.exit(1);
}

// 필수 속성 검증 (Required Property Validation)
const requiredFields = ['srcDir', 'outDir', 'sourceFiles'];
for (const field of requiredFields) {
if (CONFIG[field] === undefined || CONFIG[field] === null) {
console.error(`❌ 에러: 설정 파일에 필수 속성 '${field}'이(가) 누락되었습니다.`);
// ─── 유틸 (Utils) ───
function parseCliArgs(args) {
let allowExecutableConfig = false;
const positional = [];
for (const arg of args) {
if (arg === '--allow-executable-config') {
allowExecutableConfig = true;
} else if (arg.startsWith('-')) {
console.error(`❌ 에러: 알 수 없는 옵션입니다: ${arg}`);
process.exit(1);
} else {
positional.push(arg);
}
}
if (positional.length > 1) {
console.error('❌ 에러: 설정 파일은 하나만 지정할 수 있습니다.');
process.exit(1);
}
return { configPath: positional[0], allowExecutableConfig };
}

async function loadConfig(absPath, allowExecutableConfig) {
const ext = path.extname(absPath).toLowerCase();
if (ext === '.json') {
const parsed = JSON.parse(fs.readFileSync(absPath, 'utf-8'));
return parsed;
}
if (['.mjs', '.js', '.cjs'].includes(ext)) {
if (!allowExecutableConfig) {
throw new Error(
'Executable JavaScript configs can run arbitrary local code. ' +
'Use a JSON config, or pass --allow-executable-config for trusted configs.'
);
}
const imported = await import(pathToFileURL(absPath).href);
if (!imported || !imported.default) {
throw new Error(`설정 파일에 'default export'가 정의되어 있지 않습니다: "${absPath}"`);
}
return imported.default;
}
throw new Error(`Unsupported config extension: ${ext || '(none)'}`);
}

function validateConfig(config) {
if (!config || typeof config !== 'object' || Array.isArray(config)) {
throw new Error('Config must be an object.');
}
for (const field of ['srcDir', 'outDir']) {
if (typeof config[field] !== 'string' || config[field].trim() === '') {
throw new Error(`Config field '${field}' must be a non-empty string.`);
}
}
if (!Array.isArray(config.sourceFiles) || config.sourceFiles.length === 0 ||
config.sourceFiles.some((file) => typeof file !== 'string' || file.trim() === '')) {
throw new Error("Config field 'sourceFiles' must be an array of non-empty strings.");
}
if (config.perfData !== undefined && (!config.perfData || typeof config.perfData !== 'object' || Array.isArray(config.perfData))) {
throw new Error("Config field 'perfData' must be an object.");
}
if (config.roleRules !== undefined) {
if (!config.roleRules || typeof config.roleRules !== 'object' || Array.isArray(config.roleRules) ||
Object.entries(config.roleRules).some(([fragment, role]) => !fragment.trim() || typeof role !== 'string' || !role.trim())) {
throw new Error("Config field 'roleRules' must map non-empty name fragments to role strings.");
}
}
return config;
}

// ─── 유틸 (Utils) ───
function safeName(name) {
if (!name) return '_anonymous_';
return name.replace(/[^a-zA-Z0-9_$.]/g, '_').substring(0, 100);
Expand Down Expand Up @@ -113,7 +166,10 @@ function perfBand(pct) {
return '95_max';
}

const guessRole = CONFIG.guessRole || ((_) => 'general');
const roleRules = Object.entries(CONFIG.roleRules || {}).map(([fragment, role]) => [fragment.trim().toLowerCase(), role.trim()]);
const guessRole = typeof CONFIG.guessRole === 'function'
? CONFIG.guessRole
: (name) => roleRules.find(([fragment]) => String(name).toLowerCase().includes(fragment))?.[1] || 'general';
const OUTPUT_MARKER = '.quarkify-output';

// ─── PTX arg 의미 분류 (PTX Argument Classification) ───
Expand Down
6 changes: 3 additions & 3 deletions skills/analyze/scripts/analyze.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -98,16 +98,16 @@ export async function analyze(projectRoot = process.cwd()) {
}

const temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), 'quarkify-plugin-'));
const configPath = path.join(temporaryDirectory, 'config.mjs');
const configPath = path.join(temporaryDirectory, 'config.json');
const pluginRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
const cliPath = path.join(pluginRoot, 'quarkify.mjs');
const config = `export default ${JSON.stringify({
const config = JSON.stringify({
name: safeProjectName(root),
srcDir: root,
outDir: outputDir,
sourceFiles,
perfData: {},
}, null, 2)};\n`;
}, null, 2);

try {
await writeFile(configPath, config, { encoding: 'utf8', mode: 0o600 });
Expand Down
Loading
Loading