@vibecodetown/mcp-server 2.1.0
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/LICENSE +21 -0
- package/README.md +269 -0
- package/build/auth/gate.js +225 -0
- package/build/auth/index.js +55 -0
- package/build/auth/public_key.js +27 -0
- package/build/auth/token_cache.js +122 -0
- package/build/auth/token_verifier.js +103 -0
- package/build/bootstrap/doctor.js +115 -0
- package/build/bootstrap/installer.js +673 -0
- package/build/bootstrap/lock.js +37 -0
- package/build/bootstrap/platform.js +26 -0
- package/build/bootstrap/registry.js +37 -0
- package/build/cache/index.js +147 -0
- package/build/cli.js +101 -0
- package/build/contracts.js +22 -0
- package/build/control_plane/gate.js +161 -0
- package/build/control_plane/index.js +6 -0
- package/build/dx/activity.js +139 -0
- package/build/engine.js +106 -0
- package/build/errors.js +171 -0
- package/build/generated/activate_input.js +2 -0
- package/build/generated/activate_output.js +57 -0
- package/build/generated/advisory_review_input.js +2 -0
- package/build/generated/advisory_review_output.js +35 -0
- package/build/generated/auth_token_file.js +2 -0
- package/build/generated/briefing_input.js +2 -0
- package/build/generated/briefing_output.js +2 -0
- package/build/generated/clinic_bridge_file.js +13 -0
- package/build/generated/contracts_bundle_info.js +5 -0
- package/build/generated/create_work_order_input.js +2 -0
- package/build/generated/create_work_order_output.js +2 -0
- package/build/generated/current_work_order_file.js +2 -0
- package/build/generated/doctor_input.js +2 -0
- package/build/generated/doctor_output.js +24 -0
- package/build/generated/execution_result.js +2 -0
- package/build/generated/execution_task.js +2 -0
- package/build/generated/export_output_input.js +2 -0
- package/build/generated/export_output_output.js +2 -0
- package/build/generated/finalize_work_input.js +2 -0
- package/build/generated/finalize_work_output.js +2 -0
- package/build/generated/gate_input.js +2 -0
- package/build/generated/gate_output.js +2 -0
- package/build/generated/gate_result_v1.js +2 -0
- package/build/generated/get_decision_input.js +2 -0
- package/build/generated/get_decision_output.js +13 -0
- package/build/generated/handoff_to_clinic.js +2 -0
- package/build/generated/index.js +75 -0
- package/build/generated/inspect_code_input.js +2 -0
- package/build/generated/inspect_code_output.js +13 -0
- package/build/generated/memory_retrieve_output.js +2 -0
- package/build/generated/memory_state_file.js +2 -0
- package/build/generated/memory_status_input.js +2 -0
- package/build/generated/memory_status_output.js +13 -0
- package/build/generated/memory_sync_input.js +2 -0
- package/build/generated/memory_sync_output.js +13 -0
- package/build/generated/plugin_result.js +2 -0
- package/build/generated/react_perf_check_patterns_input.js +2 -0
- package/build/generated/react_perf_check_patterns_output.js +2 -0
- package/build/generated/react_perf_generate_report_input.js +2 -0
- package/build/generated/react_perf_generate_report_output.js +2 -0
- package/build/generated/repair_plan_input.js +2 -0
- package/build/generated/repair_plan_output.js +2 -0
- package/build/generated/run_app_input.js +2 -0
- package/build/generated/run_app_output.js +2 -0
- package/build/generated/run_state_file.js +13 -0
- package/build/generated/scaffold_input.js +2 -0
- package/build/generated/scaffold_output.js +2 -0
- package/build/generated/search_oss_input.js +2 -0
- package/build/generated/search_oss_output.js +2 -0
- package/build/generated/selection_validation_result.js +2 -0
- package/build/generated/signal_agent_input.js +2 -0
- package/build/generated/spec_high_ask_queue_items_file.js +2 -0
- package/build/generated/spec_high_clinic_bridge_output.js +2 -0
- package/build/generated/spec_high_decision_draft_output.js +2 -0
- package/build/generated/spec_high_validate_output.js +2 -0
- package/build/generated/status_input.js +2 -0
- package/build/generated/status_output.js +2 -0
- package/build/generated/submit_decision_input.js +2 -0
- package/build/generated/submit_decision_output.js +2 -0
- package/build/generated/tool_error_output.js +2 -0
- package/build/generated/undo_last_task_input.js +2 -0
- package/build/generated/undo_last_task_output.js +2 -0
- package/build/generated/update_input.js +2 -0
- package/build/generated/update_output.js +2 -0
- package/build/generated/vibe_pm_inspection_result.js +2 -0
- package/build/generated/vibe_pm_report_markdown.js +2 -0
- package/build/generated/vibe_pm_verdict.js +2 -0
- package/build/generated/vibe_repo_config.js +2 -0
- package/build/generated/vibecoding_helper_answer_output.js +2 -0
- package/build/generated/vibecoding_helper_one_loop_selection_output.js +2 -0
- package/build/generated/vibecoding_helper_show_ask_queue_output.js +2 -0
- package/build/generated/work_order_v1.js +2 -0
- package/build/generated/zoekt_evidence_input.js +2 -0
- package/build/generated/zoekt_evidence_output.js +2 -0
- package/build/index.js +111 -0
- package/build/legacy_alias.js +65 -0
- package/build/local-mode/bash.js +61 -0
- package/build/local-mode/config.js +171 -0
- package/build/local-mode/git.js +33 -0
- package/build/local-mode/init.js +110 -0
- package/build/local-mode/paths.js +24 -0
- package/build/local-mode/templates.js +856 -0
- package/build/local-mode/work-order.js +41 -0
- package/build/resources/index.js +246 -0
- package/build/security/input-validator.js +119 -0
- package/build/security/path-policy.js +289 -0
- package/build/security/sandbox.js +228 -0
- package/build/tools/react_perf/check_patterns.js +172 -0
- package/build/tools/react_perf/generate_report.js +337 -0
- package/build/tools/react_perf/index.js +119 -0
- package/build/tools/react_perf/rules/advanced.js +325 -0
- package/build/tools/react_perf/rules/async.js +104 -0
- package/build/tools/react_perf/rules/bundle.js +101 -0
- package/build/tools/react_perf/rules/client.js +186 -0
- package/build/tools/react_perf/rules/index.js +74 -0
- package/build/tools/react_perf/rules/js.js +148 -0
- package/build/tools/react_perf/rules/rendering.js +166 -0
- package/build/tools/react_perf/rules/rerender.js +161 -0
- package/build/tools/react_perf/rules/server.js +141 -0
- package/build/tools/react_perf/types.js +127 -0
- package/build/tools/vibe_pm/activate.js +102 -0
- package/build/tools/vibe_pm/advisory_review.js +77 -0
- package/build/tools/vibe_pm/briefing.js +178 -0
- package/build/tools/vibe_pm/context.js +439 -0
- package/build/tools/vibe_pm/create_work_order.js +271 -0
- package/build/tools/vibe_pm/doc_status_gate.js +370 -0
- package/build/tools/vibe_pm/doctor.js +262 -0
- package/build/tools/vibe_pm/entity_gate/preflight.js +78 -0
- package/build/tools/vibe_pm/export_output.js +135 -0
- package/build/tools/vibe_pm/finalize_work.js +393 -0
- package/build/tools/vibe_pm/gate.js +33 -0
- package/build/tools/vibe_pm/get_decision.js +281 -0
- package/build/tools/vibe_pm/index.js +593 -0
- package/build/tools/vibe_pm/inspect_code.js +828 -0
- package/build/tools/vibe_pm/intent/generator.js +294 -0
- package/build/tools/vibe_pm/intent/index.js +5 -0
- package/build/tools/vibe_pm/intent/prompt_density.js +227 -0
- package/build/tools/vibe_pm/intent/types.js +70 -0
- package/build/tools/vibe_pm/intent/verifier.js +237 -0
- package/build/tools/vibe_pm/kce/doc_usage.js +51 -0
- package/build/tools/vibe_pm/kce/on_finalize.js +11 -0
- package/build/tools/vibe_pm/kce/preflight.js +232 -0
- package/build/tools/vibe_pm/local_memory.js +26 -0
- package/build/tools/vibe_pm/memory_status.js +82 -0
- package/build/tools/vibe_pm/memory_sync.js +134 -0
- package/build/tools/vibe_pm/modules/decision_snapshot.js +29 -0
- package/build/tools/vibe_pm/modules/ensure.js +100 -0
- package/build/tools/vibe_pm/modules/fingerprint.js +30 -0
- package/build/tools/vibe_pm/modules/fix_dependencies.js +394 -0
- package/build/tools/vibe_pm/modules/planning_v1.js +110 -0
- package/build/tools/vibe_pm/modules/repo_context.js +56 -0
- package/build/tools/vibe_pm/modules/research_v1.js +114 -0
- package/build/tools/vibe_pm/modules/skills_v1.js +100 -0
- package/build/tools/vibe_pm/pm_language.js +222 -0
- package/build/tools/vibe_pm/repair_plan.js +199 -0
- package/build/tools/vibe_pm/run_app.js +597 -0
- package/build/tools/vibe_pm/run_app_podman.js +64 -0
- package/build/tools/vibe_pm/scaffold.js +550 -0
- package/build/tools/vibe_pm/search_oss.js +124 -0
- package/build/tools/vibe_pm/status.js +153 -0
- package/build/tools/vibe_pm/submit_decision.js +87 -0
- package/build/tools/vibe_pm/system_design/issue_mapping.js +47 -0
- package/build/tools/vibe_pm/system_design/rulebook.js +112 -0
- package/build/tools/vibe_pm/system_design/semgrep.js +132 -0
- package/build/tools/vibe_pm/types.js +229 -0
- package/build/tools/vibe_pm/undo_last_task.js +163 -0
- package/build/tools/vibe_pm/update.js +146 -0
- package/build/tools/vibe_pm/zoekt_evidence.js +96 -0
- package/build/tools.js +269 -0
- package/build/version-check.js +239 -0
- package/build/vibe-cli.js +631 -0
- package/package.json +76 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/tools/vibe_pm/status.ts
|
|
2
|
+
// vibe_pm.status - Project status query
|
|
3
|
+
import { runEngineWithCache } from "../../engine.js";
|
|
4
|
+
import { safeJsonParse } from "../../cli.js";
|
|
5
|
+
import { resolveRunId, resolveProjectId, getRunContext, readRunState } from "./context.js";
|
|
6
|
+
import { formatMode, formatPhase, signalToReview } from "./pm_language.js";
|
|
7
|
+
import { validateToolInput } from "../../security/input-validator.js";
|
|
8
|
+
import { SpecHighValidateOutputSchema } from "../../generated/spec_high_validate_output.js";
|
|
9
|
+
/**
|
|
10
|
+
* vibe_pm.status - Query current project status
|
|
11
|
+
*
|
|
12
|
+
* Internal mapping:
|
|
13
|
+
* → spec-high validate <run_id>
|
|
14
|
+
* → status summary generation
|
|
15
|
+
*/
|
|
16
|
+
export async function status(input) {
|
|
17
|
+
const basePath = process.cwd();
|
|
18
|
+
validateToolInput({ project_id: input.project_id });
|
|
19
|
+
// Resolve run_id
|
|
20
|
+
const { run_id, is_new } = resolveRunId(input.project_id, basePath);
|
|
21
|
+
const project_id = resolveProjectId(run_id, basePath);
|
|
22
|
+
// If no existing run, return empty status
|
|
23
|
+
if (is_new) {
|
|
24
|
+
return {
|
|
25
|
+
project_id,
|
|
26
|
+
project: {
|
|
27
|
+
name: project_id,
|
|
28
|
+
mode: "미설정",
|
|
29
|
+
started_at: "없음"
|
|
30
|
+
},
|
|
31
|
+
current_state: {
|
|
32
|
+
phase: "프로젝트가 없습니다",
|
|
33
|
+
decisions_made: 0,
|
|
34
|
+
decisions_pending: 0
|
|
35
|
+
},
|
|
36
|
+
risks: ["프로젝트를 먼저 시작해주세요"],
|
|
37
|
+
next_action: {
|
|
38
|
+
tool: "vibe_pm.briefing",
|
|
39
|
+
label: "다음 할 일: 프로젝트 시작하기"
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// Get run context and state
|
|
44
|
+
const context = getRunContext(run_id, basePath);
|
|
45
|
+
const runState = readRunState(run_id, basePath);
|
|
46
|
+
// Run spec-high validate to get current status (with caching)
|
|
47
|
+
let validateResult = null;
|
|
48
|
+
try {
|
|
49
|
+
const { code, stdout } = await runEngineWithCache("spec-high", ["--root", "engines/spec_high", "validate", run_id], {
|
|
50
|
+
timeoutMs: 60_000
|
|
51
|
+
});
|
|
52
|
+
if (code === 0 && stdout) {
|
|
53
|
+
const parsed = safeJsonParse(stdout);
|
|
54
|
+
if (parsed.ok) {
|
|
55
|
+
const validated = SpecHighValidateOutputSchema.safeParse(parsed.value);
|
|
56
|
+
if (validated.success)
|
|
57
|
+
validateResult = validated.data;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Continue with state-based status if validate fails
|
|
63
|
+
}
|
|
64
|
+
// Determine phase and status from various sources
|
|
65
|
+
const phase = runState?.phase ?? validateResult?.phase ?? "unknown";
|
|
66
|
+
const mode = runState?.mode ?? "balanced";
|
|
67
|
+
const decisionsMade = validateResult?.decisions_made ?? runState?.decisions_made ?? 0;
|
|
68
|
+
const decisionsPending = validateResult?.decisions_pending ?? runState?.decisions_pending ?? 0;
|
|
69
|
+
// Get last review status
|
|
70
|
+
let lastReviewStatus;
|
|
71
|
+
if (validateResult?.signal_level) {
|
|
72
|
+
lastReviewStatus = signalToReview(validateResult.signal_level).result;
|
|
73
|
+
}
|
|
74
|
+
else if (runState?.last_review_status) {
|
|
75
|
+
lastReviewStatus = runState.last_review_status;
|
|
76
|
+
}
|
|
77
|
+
// Collect risks
|
|
78
|
+
const risks = validateResult?.risks ?? [];
|
|
79
|
+
// Determine next action based on state
|
|
80
|
+
const nextAction = determineNextAction(phase, decisionsPending, lastReviewStatus);
|
|
81
|
+
return {
|
|
82
|
+
project_id,
|
|
83
|
+
project: {
|
|
84
|
+
name: project_id,
|
|
85
|
+
mode: formatMode(mode),
|
|
86
|
+
started_at: runState?.created_at ?? "알 수 없음"
|
|
87
|
+
},
|
|
88
|
+
current_state: {
|
|
89
|
+
phase: formatPhase(phase),
|
|
90
|
+
decisions_made: decisionsMade,
|
|
91
|
+
decisions_pending: decisionsPending,
|
|
92
|
+
last_review_status: lastReviewStatus
|
|
93
|
+
},
|
|
94
|
+
risks,
|
|
95
|
+
next_action: nextAction
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function determineNextAction(phase, decisionsPending, lastReviewStatus) {
|
|
99
|
+
// If there are pending decisions
|
|
100
|
+
if (decisionsPending > 0) {
|
|
101
|
+
return {
|
|
102
|
+
tool: "vibe_pm.get_decision",
|
|
103
|
+
label: `다음 할 일: 결재 ${decisionsPending}건 처리`
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// Based on phase
|
|
107
|
+
switch (phase) {
|
|
108
|
+
case "init":
|
|
109
|
+
case "briefing":
|
|
110
|
+
return {
|
|
111
|
+
tool: "vibe_pm.get_decision",
|
|
112
|
+
label: "다음 할 일: 결재 요청 확인"
|
|
113
|
+
};
|
|
114
|
+
case "decision":
|
|
115
|
+
return {
|
|
116
|
+
tool: "vibe_pm.create_work_order",
|
|
117
|
+
label: "다음 할 일: 작업 지시서 발행"
|
|
118
|
+
};
|
|
119
|
+
case "work_order":
|
|
120
|
+
case "implementation":
|
|
121
|
+
return {
|
|
122
|
+
tool: "vibe_pm.inspect_code",
|
|
123
|
+
label: "다음 할 일: 구현 후 검수 요청"
|
|
124
|
+
};
|
|
125
|
+
case "review":
|
|
126
|
+
if (lastReviewStatus === "GO") {
|
|
127
|
+
return {
|
|
128
|
+
tool: "vibe_pm.create_work_order",
|
|
129
|
+
label: "다음 할 일: 다음 작업 지시서 받기"
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
else if (lastReviewStatus === "FIX" || lastReviewStatus === "BLOCK") {
|
|
133
|
+
return {
|
|
134
|
+
tool: "vibe_pm.repair_plan",
|
|
135
|
+
label: "다음 할 일: 수정 계획 확인"
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
tool: "vibe_pm.inspect_code",
|
|
140
|
+
label: "다음 할 일: 검수 재시도"
|
|
141
|
+
};
|
|
142
|
+
case "completed":
|
|
143
|
+
return {
|
|
144
|
+
tool: "vibe_pm.briefing",
|
|
145
|
+
label: "다음 할 일: 새 프로젝트 시작"
|
|
146
|
+
};
|
|
147
|
+
default:
|
|
148
|
+
return {
|
|
149
|
+
tool: "vibe_pm.status",
|
|
150
|
+
label: "다음 할 일: 상태 확인"
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/tools/vibe_pm/submit_decision.ts
|
|
2
|
+
// vibe_pm.submit_decision - Submit decision
|
|
3
|
+
import { runEngine, invalidateEngineCache } from "../../engine.js";
|
|
4
|
+
import { safeJsonParse } from "../../cli.js";
|
|
5
|
+
import { resolveRunId, resolveProjectId, invalidateStateCache } from "./context.js";
|
|
6
|
+
import { validateToolInput } from "../../security/input-validator.js";
|
|
7
|
+
import { VibecodingHelperAnswerOutputSchema } from "../../generated/vibecoding_helper_answer_output.js";
|
|
8
|
+
/**
|
|
9
|
+
* vibe_pm.submit_decision - Submit an approval decision
|
|
10
|
+
*
|
|
11
|
+
* Internal mapping:
|
|
12
|
+
* → spec-high answer <run_id> --text <choice>
|
|
13
|
+
* → spec-high derive <run_id>
|
|
14
|
+
*/
|
|
15
|
+
export async function submitDecision(input) {
|
|
16
|
+
const basePath = process.cwd();
|
|
17
|
+
validateToolInput({ project_id: input.project_id, note: input.note });
|
|
18
|
+
// Resolve run_id
|
|
19
|
+
const { run_id } = resolveRunId(input.project_id, basePath);
|
|
20
|
+
const project_id = resolveProjectId(run_id, basePath);
|
|
21
|
+
// Format answer text (combine choice with note if provided)
|
|
22
|
+
const answerText = input.note ? `${input.choice}: ${input.note}` : input.choice;
|
|
23
|
+
// Step 1: Submit answer
|
|
24
|
+
const answerResult = await runEngine("vibecoding-helper", ["answer", run_id, "--text", answerText], { timeoutMs: 120_000 });
|
|
25
|
+
if (answerResult.code !== 0) {
|
|
26
|
+
throw new Error(`결재 제출 실패: ${answerResult.stderr || "알 수 없는 오류"}`);
|
|
27
|
+
}
|
|
28
|
+
// Parse answer response
|
|
29
|
+
let remaining = 0;
|
|
30
|
+
const answerParsed = safeJsonParse(answerResult.stdout);
|
|
31
|
+
if (!answerParsed.ok) {
|
|
32
|
+
throw new Error("결재 결과를 파싱할 수 없습니다. 업데이트 후 다시 시도해주세요.");
|
|
33
|
+
}
|
|
34
|
+
const validated = VibecodingHelperAnswerOutputSchema.safeParse(answerParsed.value);
|
|
35
|
+
if (!validated.success) {
|
|
36
|
+
throw new Error("결재 결과 형식이 예상과 다릅니다. 업데이트 후 다시 시도해주세요.");
|
|
37
|
+
}
|
|
38
|
+
remaining = validated.data.remaining ?? validated.data.ask_queue_remaining ?? 0;
|
|
39
|
+
// Step 2: Run derive to propagate decisions
|
|
40
|
+
try {
|
|
41
|
+
await runEngine("spec-high", ["--root", "engines/spec_high", "derive", run_id], { timeoutMs: 60_000 });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Derive failure is non-fatal, continue
|
|
45
|
+
}
|
|
46
|
+
// Step 3: Invalidate caches after state mutation
|
|
47
|
+
invalidateEngineCache(new RegExp(run_id));
|
|
48
|
+
invalidateStateCache(run_id, basePath);
|
|
49
|
+
// Generate summary based on choice
|
|
50
|
+
const summary = generateDecisionSummary(input.choice, input.note);
|
|
51
|
+
// Determine next action
|
|
52
|
+
const nextAction = remaining > 0
|
|
53
|
+
? {
|
|
54
|
+
tool: "vibe_pm.get_decision",
|
|
55
|
+
reason: `아직 ${remaining}건의 결재가 남아있습니다`
|
|
56
|
+
}
|
|
57
|
+
: {
|
|
58
|
+
tool: "vibe_pm.create_work_order",
|
|
59
|
+
reason: "모든 결재가 완료되었습니다. 작업 지시서를 발행합니다"
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
project_id,
|
|
63
|
+
result: {
|
|
64
|
+
decision_id: input.decision_id,
|
|
65
|
+
chosen: input.choice,
|
|
66
|
+
summary
|
|
67
|
+
},
|
|
68
|
+
remaining_decisions: remaining,
|
|
69
|
+
next_action: nextAction
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Generate user-friendly decision summary
|
|
74
|
+
*/
|
|
75
|
+
function generateDecisionSummary(choice, note) {
|
|
76
|
+
const choiceSummaries = {
|
|
77
|
+
A: "첫 번째 옵션으로 결정되었습니다",
|
|
78
|
+
B: "두 번째 옵션으로 결정되었습니다",
|
|
79
|
+
C: "세 번째 옵션으로 결정되었습니다",
|
|
80
|
+
U: "추가 검토가 필요한 것으로 표시되었습니다"
|
|
81
|
+
};
|
|
82
|
+
let summary = choiceSummaries[choice] ?? `${choice} 옵션으로 결정되었습니다`;
|
|
83
|
+
if (note) {
|
|
84
|
+
summary += ` (참고: ${note})`;
|
|
85
|
+
}
|
|
86
|
+
return summary;
|
|
87
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { lookupRuleMeta } from "./rulebook.js";
|
|
2
|
+
export function designRiskForPrinciple(principleId) {
|
|
3
|
+
switch (principleId) {
|
|
4
|
+
case "STRATEGY_ENFORCEMENT":
|
|
5
|
+
return "분기 로직이 비대해져 변경/확장이 어려워질 수 있습니다.";
|
|
6
|
+
case "ADAPTER_ENFORCEMENT":
|
|
7
|
+
return "외부 의존이 섞이면 테스트/보안/교체 비용이 급증할 수 있습니다.";
|
|
8
|
+
case "OBSERVER_ENFORCEMENT":
|
|
9
|
+
return "이벤트 흐름이 분산되면 추적/재현이 어려워질 수 있습니다.";
|
|
10
|
+
case "BUILDER_ENFORCEMENT":
|
|
11
|
+
return "옵션 폭발로 유지보수 비용이 증가할 수 있습니다.";
|
|
12
|
+
case "SINGLETON_GUARD":
|
|
13
|
+
return "전역 mutable 상태는 테스트/동시성 버그를 유발할 수 있습니다.";
|
|
14
|
+
case "UNMAPPED_RULE":
|
|
15
|
+
default:
|
|
16
|
+
return "설계 규칙 위반 가능성이 있어 확인이 필요합니다.";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function semgrepFindingsToIssues(args) {
|
|
20
|
+
const issues = [];
|
|
21
|
+
let error_count = 0;
|
|
22
|
+
for (const f of args.findings) {
|
|
23
|
+
const meta = lookupRuleMeta(args.rulebook, f.check_id);
|
|
24
|
+
const principleId = meta?.principle_id ?? "UNMAPPED_RULE";
|
|
25
|
+
const patternName = meta?.pattern ?? null;
|
|
26
|
+
if (f.severity === "ERROR" || f.severity === "CRITICAL") {
|
|
27
|
+
error_count += 1;
|
|
28
|
+
}
|
|
29
|
+
issues.push({
|
|
30
|
+
type: "RULE_VIOLATION",
|
|
31
|
+
business_risk: designRiskForPrinciple(principleId),
|
|
32
|
+
plain_explanation: f.message,
|
|
33
|
+
technical_detail: {
|
|
34
|
+
file_path: f.path,
|
|
35
|
+
line_range: f.start_line && f.end_line ? [f.start_line, f.end_line] : undefined,
|
|
36
|
+
rule_violated: f.check_id,
|
|
37
|
+
principle_id: principleId,
|
|
38
|
+
pattern: patternName ?? undefined,
|
|
39
|
+
rule_engine: "semgrep",
|
|
40
|
+
rule_id: f.check_id,
|
|
41
|
+
severity: f.severity,
|
|
42
|
+
evidence_ref: args.evidence_ref
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return { issues, error_count };
|
|
47
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
const RuleMetaSchema = z.object({
|
|
5
|
+
principle_id: z.string().min(1),
|
|
6
|
+
pattern: z.string().min(1).nullable().optional(),
|
|
7
|
+
default_severity: z.string().min(1).optional(),
|
|
8
|
+
notes: z.string().optional()
|
|
9
|
+
});
|
|
10
|
+
const RulebookSchema = z.object({
|
|
11
|
+
version: z.string().optional(),
|
|
12
|
+
engine: z.string().optional(),
|
|
13
|
+
rules: z.record(RuleMetaSchema).default({})
|
|
14
|
+
});
|
|
15
|
+
const BUILTIN_RULEBOOK = {
|
|
16
|
+
version: "v1",
|
|
17
|
+
engine: "semgrep",
|
|
18
|
+
rules: {
|
|
19
|
+
"strategy-no-long-if-elif-chain-python": {
|
|
20
|
+
principle_id: "STRATEGY_ENFORCEMENT",
|
|
21
|
+
pattern: "Strategy",
|
|
22
|
+
default_severity: "WARNING"
|
|
23
|
+
},
|
|
24
|
+
"strategy-no-long-else-if-chain-ts": {
|
|
25
|
+
principle_id: "STRATEGY_ENFORCEMENT",
|
|
26
|
+
pattern: "Strategy",
|
|
27
|
+
default_severity: "WARNING"
|
|
28
|
+
},
|
|
29
|
+
"adapter-no-network-in-core-python": {
|
|
30
|
+
principle_id: "ADAPTER_ENFORCEMENT",
|
|
31
|
+
pattern: "Adapter",
|
|
32
|
+
default_severity: "WARNING"
|
|
33
|
+
},
|
|
34
|
+
"adapter-no-network-in-core-ts": {
|
|
35
|
+
principle_id: "ADAPTER_ENFORCEMENT",
|
|
36
|
+
pattern: "Adapter",
|
|
37
|
+
default_severity: "WARNING"
|
|
38
|
+
},
|
|
39
|
+
"observer-publish-only-in-events-python": {
|
|
40
|
+
principle_id: "OBSERVER_ENFORCEMENT",
|
|
41
|
+
pattern: "Observer",
|
|
42
|
+
default_severity: "WARNING"
|
|
43
|
+
},
|
|
44
|
+
"observer-publish-only-in-events-ts": {
|
|
45
|
+
principle_id: "OBSERVER_ENFORCEMENT",
|
|
46
|
+
pattern: "Observer",
|
|
47
|
+
default_severity: "WARNING"
|
|
48
|
+
},
|
|
49
|
+
"observer-no-eventbus-instantiation-outside-factory-ts": {
|
|
50
|
+
principle_id: "OBSERVER_ENFORCEMENT",
|
|
51
|
+
pattern: "Observer",
|
|
52
|
+
default_severity: "WARNING"
|
|
53
|
+
},
|
|
54
|
+
"observer-no-eventbus-getinstance-outside-events-ts": {
|
|
55
|
+
principle_id: "OBSERVER_ENFORCEMENT",
|
|
56
|
+
pattern: "Observer",
|
|
57
|
+
default_severity: "WARNING"
|
|
58
|
+
},
|
|
59
|
+
"observer-no-eventbus-instantiation-outside-factory-python": {
|
|
60
|
+
principle_id: "OBSERVER_ENFORCEMENT",
|
|
61
|
+
pattern: "Observer",
|
|
62
|
+
default_severity: "WARNING"
|
|
63
|
+
},
|
|
64
|
+
"observer-no-eventbus-getinstance-outside-events-python": {
|
|
65
|
+
principle_id: "OBSERVER_ENFORCEMENT",
|
|
66
|
+
pattern: "Observer",
|
|
67
|
+
default_severity: "WARNING"
|
|
68
|
+
},
|
|
69
|
+
"builder-too-many-init-params-python": {
|
|
70
|
+
principle_id: "BUILDER_ENFORCEMENT",
|
|
71
|
+
pattern: "Builder",
|
|
72
|
+
default_severity: "WARNING"
|
|
73
|
+
},
|
|
74
|
+
"builder-too-many-constructor-params-ts": {
|
|
75
|
+
principle_id: "BUILDER_ENFORCEMENT",
|
|
76
|
+
pattern: "Builder",
|
|
77
|
+
default_severity: "WARNING"
|
|
78
|
+
},
|
|
79
|
+
"singleton-no-global-mutable-python": {
|
|
80
|
+
principle_id: "SINGLETON_GUARD",
|
|
81
|
+
pattern: "Singleton",
|
|
82
|
+
default_severity: "WARNING"
|
|
83
|
+
},
|
|
84
|
+
"singleton-no-exported-mutable-singletons-ts": {
|
|
85
|
+
principle_id: "SINGLETON_GUARD",
|
|
86
|
+
pattern: "Singleton",
|
|
87
|
+
default_severity: "WARNING"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
function repoRulebookPath(basePath) {
|
|
92
|
+
return path.join(basePath, "docs", "DEV_SPEC", "system_design_mapping", "RULEBOOK_SEMGREP_MAP.v1.json");
|
|
93
|
+
}
|
|
94
|
+
export function loadRulebook(basePath) {
|
|
95
|
+
const candidate = repoRulebookPath(basePath);
|
|
96
|
+
if (!fs.existsSync(candidate))
|
|
97
|
+
return { source: "builtin", rulebook: BUILTIN_RULEBOOK };
|
|
98
|
+
try {
|
|
99
|
+
const raw = JSON.parse(fs.readFileSync(candidate, "utf-8"));
|
|
100
|
+
const parsed = RulebookSchema.safeParse(raw);
|
|
101
|
+
if (parsed.success)
|
|
102
|
+
return { source: "repo_file", rulebook: parsed.data };
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// ignore
|
|
106
|
+
}
|
|
107
|
+
return { source: "builtin", rulebook: BUILTIN_RULEBOOK };
|
|
108
|
+
}
|
|
109
|
+
export function lookupRuleMeta(rulebook, checkId) {
|
|
110
|
+
const meta = (rulebook.rules ?? {})[checkId];
|
|
111
|
+
return meta ?? null;
|
|
112
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { runCmd, safeJsonParse } from "../../../cli.js";
|
|
5
|
+
import { resolveRunDir } from "../context.js";
|
|
6
|
+
const SemgrepResultSchema = z.object({
|
|
7
|
+
results: z
|
|
8
|
+
.array(z.object({
|
|
9
|
+
check_id: z.string().optional(),
|
|
10
|
+
path: z.string().optional(),
|
|
11
|
+
start: z
|
|
12
|
+
.object({
|
|
13
|
+
line: z.number().int().positive().optional(),
|
|
14
|
+
col: z.number().int().positive().optional()
|
|
15
|
+
})
|
|
16
|
+
.optional(),
|
|
17
|
+
end: z
|
|
18
|
+
.object({
|
|
19
|
+
line: z.number().int().positive().optional(),
|
|
20
|
+
col: z.number().int().positive().optional()
|
|
21
|
+
})
|
|
22
|
+
.optional(),
|
|
23
|
+
extra: z
|
|
24
|
+
.object({
|
|
25
|
+
message: z.string().optional(),
|
|
26
|
+
severity: z.string().optional()
|
|
27
|
+
})
|
|
28
|
+
.optional()
|
|
29
|
+
}))
|
|
30
|
+
.default([])
|
|
31
|
+
});
|
|
32
|
+
function findConfigPath(basePath) {
|
|
33
|
+
const candidates = [
|
|
34
|
+
path.join(basePath, "semgrep", "rules", "design_patterns.yml"),
|
|
35
|
+
path.join(basePath, "semgrep", "rules", "design_patterns.yaml"),
|
|
36
|
+
path.join(basePath, "docs", "DEV_SPEC", "system_design_mapping", "semgrep_design_enforcement_rules.yml")
|
|
37
|
+
];
|
|
38
|
+
for (const c of candidates) {
|
|
39
|
+
if (fs.existsSync(c))
|
|
40
|
+
return c;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
function tryWriteEvidence(basePath, run_id, stdout) {
|
|
45
|
+
if (!stdout.trim())
|
|
46
|
+
return null;
|
|
47
|
+
const resolved = resolveRunDir(run_id, basePath);
|
|
48
|
+
if (!resolved)
|
|
49
|
+
return null;
|
|
50
|
+
try {
|
|
51
|
+
const abs = path.join(resolved.run_dir, "evidence", "semgrep_results.json");
|
|
52
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
53
|
+
fs.writeFileSync(abs, stdout, "utf-8");
|
|
54
|
+
return path.relative(basePath, abs).replace(/\\\\/g, "/");
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function runSemgrepDesignEnforcement(args) {
|
|
61
|
+
const configPath = findConfigPath(args.basePath);
|
|
62
|
+
if (!configPath) {
|
|
63
|
+
return {
|
|
64
|
+
status: "SKIPPED",
|
|
65
|
+
findings: [],
|
|
66
|
+
error: { type: "config_missing", message: "design semgrep config not found" }
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const targetArgs = [];
|
|
70
|
+
for (const t of args.targets) {
|
|
71
|
+
if (!t)
|
|
72
|
+
continue;
|
|
73
|
+
const abs = path.isAbsolute(t) ? t : path.join(args.basePath, t);
|
|
74
|
+
if (!fs.existsSync(abs))
|
|
75
|
+
continue;
|
|
76
|
+
targetArgs.push(path.isAbsolute(t) ? t : t.replace(/\\\\/g, "/"));
|
|
77
|
+
}
|
|
78
|
+
if (targetArgs.length === 0) {
|
|
79
|
+
return { status: "SKIPPED", findings: [] };
|
|
80
|
+
}
|
|
81
|
+
const cmdArgs = ["scan", "--config", configPath, "--json", "--quiet", "--metrics", "off", ...targetArgs];
|
|
82
|
+
const { code, stdout, stderr } = await runCmd("semgrep", cmdArgs, {
|
|
83
|
+
cwd: args.basePath,
|
|
84
|
+
timeoutMs: args.timeoutMs ?? 60_000
|
|
85
|
+
});
|
|
86
|
+
const evidence_ref = tryWriteEvidence(args.basePath, args.run_id, stdout) ?? undefined;
|
|
87
|
+
if (code !== 0) {
|
|
88
|
+
return {
|
|
89
|
+
status: "ERROR",
|
|
90
|
+
findings: [],
|
|
91
|
+
evidence_ref,
|
|
92
|
+
error: { type: "semgrep_failed", message: `semgrep exit_code=${code}`, stderr: (stderr ?? "").slice(0, 2000) }
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const parsedJson = safeJsonParse(stdout);
|
|
96
|
+
if (!parsedJson.ok) {
|
|
97
|
+
return {
|
|
98
|
+
status: "ERROR",
|
|
99
|
+
findings: [],
|
|
100
|
+
evidence_ref,
|
|
101
|
+
error: { type: "json_parse_error", message: parsedJson.error, stderr: (stderr ?? "").slice(0, 2000) }
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const validated = SemgrepResultSchema.safeParse(parsedJson.value);
|
|
105
|
+
if (!validated.success) {
|
|
106
|
+
return {
|
|
107
|
+
status: "ERROR",
|
|
108
|
+
findings: [],
|
|
109
|
+
evidence_ref,
|
|
110
|
+
error: { type: "schema_mismatch", message: "unexpected semgrep json shape", stderr: (stderr ?? "").slice(0, 2000) }
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const findings = [];
|
|
114
|
+
for (const r of validated.data.results) {
|
|
115
|
+
const check_id = (r.check_id ?? "").trim();
|
|
116
|
+
const p = (r.path ?? "").trim();
|
|
117
|
+
if (!check_id || !p)
|
|
118
|
+
continue;
|
|
119
|
+
const sevRaw = (r.extra?.severity ?? "").trim().toUpperCase();
|
|
120
|
+
const severity = sevRaw || "WARNING";
|
|
121
|
+
const message = (r.extra?.message ?? "").trim() || "설계 규칙 위반 가능성이 있습니다";
|
|
122
|
+
findings.push({
|
|
123
|
+
check_id,
|
|
124
|
+
path: p.replace(/\\\\/g, "/"),
|
|
125
|
+
start_line: r.start?.line,
|
|
126
|
+
end_line: r.end?.line,
|
|
127
|
+
severity,
|
|
128
|
+
message
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return { status: "OK", findings, evidence_ref };
|
|
132
|
+
}
|