vibepro 0.1.0-alpha.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 +201 -0
- package/NOTICE +9 -0
- package/README.ja.md +448 -0
- package/README.md +520 -0
- package/agent-instructions/codex/AGENTS.vibepro.md +45 -0
- package/bin/vibepro.js +9 -0
- package/docs/assets/vibepro-header.png +0 -0
- package/package.json +51 -0
- package/skills/vibepro-diagnosis-packages/SKILL.md +133 -0
- package/skills/vibepro-human-review/SKILL.md +73 -0
- package/skills/vibepro-story-refactor/SKILL.md +89 -0
- package/skills/vibepro-workflow/SKILL.md +139 -0
- package/src/agent-harness-map.js +230 -0
- package/src/agent-harness-scanner.js +337 -0
- package/src/agent-review.js +2180 -0
- package/src/api-boundary-scanner.js +452 -0
- package/src/architecture-profiler.js +423 -0
- package/src/authorization-scoring.js +149 -0
- package/src/brainbase-importer.js +534 -0
- package/src/change-risk-classifier.js +195 -0
- package/src/check-packs.js +605 -0
- package/src/checkpoint-manager.js +233 -0
- package/src/cli.js +2213 -0
- package/src/code-quality-scanner.js +310 -0
- package/src/codex-manager.js +143 -0
- package/src/component-style-scanner.js +336 -0
- package/src/coverage-report.js +99 -0
- package/src/database-access-scanner.js +163 -0
- package/src/decision-records.js +315 -0
- package/src/design-modernize.js +1435 -0
- package/src/design-system.js +1732 -0
- package/src/diagnostic-engine.js +1945 -0
- package/src/diagram-requirement-resolver.js +194 -0
- package/src/doctor.js +677 -0
- package/src/environment-graph.js +424 -0
- package/src/execution-state.js +849 -0
- package/src/explore-evidence.js +425 -0
- package/src/flow-design-scanner.js +896 -0
- package/src/flow-verifier.js +887 -0
- package/src/gesture-interaction-scanner.js +330 -0
- package/src/graph-context.js +263 -0
- package/src/graphify-adapter.js +189 -0
- package/src/html-report.js +1035 -0
- package/src/journey-map.js +1299 -0
- package/src/language.js +48 -0
- package/src/lazy-pattern-detector.js +182 -0
- package/src/local-dev-scanner.js +135 -0
- package/src/managed-worktree-gate.js +187 -0
- package/src/managed-worktree.js +766 -0
- package/src/merge-manager.js +501 -0
- package/src/network-contract-scanner.js +442 -0
- package/src/nocodb-story-sync.js +386 -0
- package/src/oss-readiness-scanner.js +417 -0
- package/src/performance-evidence.js +756 -0
- package/src/performance-measurer.js +591 -0
- package/src/pr-manager.js +8220 -0
- package/src/presets.js +682 -0
- package/src/public-discovery-scanner.js +519 -0
- package/src/refactoring-delta-reporter.js +367 -0
- package/src/refactoring-opportunity-generator.js +797 -0
- package/src/regression-risk-scanner.js +146 -0
- package/src/repo-status.js +266 -0
- package/src/report-fingerprint.js +188 -0
- package/src/report-pr-body-prompt-template.md +108 -0
- package/src/report-pr-body-schema.json +95 -0
- package/src/report-store.js +135 -0
- package/src/report-validator.js +192 -0
- package/src/requirement-consistency.js +1066 -0
- package/src/runtime-info.js +134 -0
- package/src/self-dogfood-scanner.js +476 -0
- package/src/session-learning.js +164 -0
- package/src/skills-manager.js +157 -0
- package/src/spec-drift.js +378 -0
- package/src/spec-fingerprint.js +445 -0
- package/src/spec-prompt-template.md +155 -0
- package/src/spec-schema.json +219 -0
- package/src/spec-store.js +258 -0
- package/src/spec-validator.js +459 -0
- package/src/static-site-scanner.js +316 -0
- package/src/story-candidate-generator.js +85 -0
- package/src/story-catalog-generator.js +2813 -0
- package/src/story-html.js +156 -0
- package/src/story-manager.js +2144 -0
- package/src/story-task-generator.js +522 -0
- package/src/task-manager.js +1029 -0
- package/src/terminal-link-scanner.js +238 -0
- package/src/usage-report.js +417 -0
- package/src/verification-evidence.js +284 -0
- package/src/workspace.js +126 -0
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
const INFO_GATE_EFFECT = 'info';
|
|
2
|
+
|
|
3
|
+
export function buildRefactoringOpportunities(evidence) {
|
|
4
|
+
const codeQuality = evidence?.code_quality ?? {};
|
|
5
|
+
const opportunities = [
|
|
6
|
+
...buildDuplicateQueryOpportunities(codeQuality.duplicate_query_shapes ?? []),
|
|
7
|
+
...buildResponsibilityHotspotOpportunities(codeQuality.responsibility_hotspots ?? [])
|
|
8
|
+
];
|
|
9
|
+
return rankRefactoringOpportunities(opportunities, evidence);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildRefactoringCampaigns(evidence) {
|
|
13
|
+
const opportunities = Array.isArray(evidence?.refactoring_opportunities)
|
|
14
|
+
? evidence.refactoring_opportunities
|
|
15
|
+
: [];
|
|
16
|
+
const groups = new Map();
|
|
17
|
+
for (const opportunity of opportunities) {
|
|
18
|
+
const key = buildCampaignKey(opportunity);
|
|
19
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
20
|
+
groups.get(key).push(opportunity);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return [...groups.values()]
|
|
24
|
+
.map((items, index) => buildRefactoringCampaign(items, index + 1))
|
|
25
|
+
.sort((a, b) => b.score.total - a.score.total || a.id.localeCompare(b.id))
|
|
26
|
+
.map((campaign, index) => ({
|
|
27
|
+
...campaign,
|
|
28
|
+
id: `VP-CAMPAIGN-REF-${formatSerial(index + 1)}`,
|
|
29
|
+
rank: index + 1
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function buildRefactoringActionCandidates(evidence) {
|
|
34
|
+
const opportunities = Array.isArray(evidence?.refactoring_opportunities)
|
|
35
|
+
? evidence.refactoring_opportunities
|
|
36
|
+
: [];
|
|
37
|
+
const campaigns = Array.isArray(evidence?.refactoring_campaigns)
|
|
38
|
+
? evidence.refactoring_campaigns
|
|
39
|
+
: [];
|
|
40
|
+
const primaryDryOpportunity = opportunities.find((opportunity) => (
|
|
41
|
+
opportunity.finding_id === 'VP-DRY-001'
|
|
42
|
+
&& opportunity.source === 'duplicate_query_shape'
|
|
43
|
+
));
|
|
44
|
+
const primaryArchOpportunity = opportunities.find((opportunity) => (
|
|
45
|
+
opportunity.finding_id === 'VP-ARCH-001'
|
|
46
|
+
&& opportunity.source === 'responsibility_hotspot'
|
|
47
|
+
));
|
|
48
|
+
return [
|
|
49
|
+
primaryDryOpportunity
|
|
50
|
+
? buildRefactoringActionCandidate({
|
|
51
|
+
actionId: 'VP-ACTION-DRY-001',
|
|
52
|
+
opportunity: primaryDryOpportunity,
|
|
53
|
+
campaign: findCampaignForOpportunity(campaigns, primaryDryOpportunity.id)
|
|
54
|
+
})
|
|
55
|
+
: null,
|
|
56
|
+
primaryArchOpportunity
|
|
57
|
+
? buildRefactoringActionCandidate({
|
|
58
|
+
actionId: 'VP-ACTION-ARCH-001',
|
|
59
|
+
opportunity: primaryArchOpportunity,
|
|
60
|
+
campaign: findCampaignForOpportunity(campaigns, primaryArchOpportunity.id)
|
|
61
|
+
})
|
|
62
|
+
: null
|
|
63
|
+
].filter(Boolean);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildRefactoringActionCandidate({ actionId, opportunity, campaign }) {
|
|
67
|
+
const title = campaign?.title ?? opportunity.title;
|
|
68
|
+
const graphContext = campaign?.graph_context ?? opportunity.graph_context ?? null;
|
|
69
|
+
return {
|
|
70
|
+
id: actionId,
|
|
71
|
+
finding_id: opportunity.finding_id,
|
|
72
|
+
scope: 'refactoring',
|
|
73
|
+
title,
|
|
74
|
+
target_count: campaign?.target_count ?? opportunity.target_count,
|
|
75
|
+
target_files: campaign?.target_files ?? opportunity.target_files,
|
|
76
|
+
execution_policy: 'proposal_only',
|
|
77
|
+
mutates_repository: false,
|
|
78
|
+
confidence: opportunity.confidence,
|
|
79
|
+
recommendation: campaign?.story_blueprint?.summary ?? opportunity.story_blueprint.summary,
|
|
80
|
+
refactoring_opportunity_id: opportunity.id,
|
|
81
|
+
refactoring_campaign_id: campaign?.id ?? null,
|
|
82
|
+
graph_context: graphContext,
|
|
83
|
+
story_blueprint: campaign?.story_blueprint ?? opportunity.story_blueprint,
|
|
84
|
+
implementation_plan: buildRefactoringImplementationPlan(opportunity, campaign, graphContext)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildDuplicateQueryOpportunities(duplicateQueryShapes) {
|
|
89
|
+
return duplicateQueryShapes
|
|
90
|
+
.filter((shape) => shape?.gate_effect !== INFO_GATE_EFFECT)
|
|
91
|
+
.map((shape, index) => buildDuplicateQueryOpportunity(shape, index + 1))
|
|
92
|
+
.sort(compareOpportunities);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildDuplicateQueryOpportunity(shape, serialNumber) {
|
|
96
|
+
const parsed = parseQuerySignature(shape.signature);
|
|
97
|
+
const intent = classifyQueryIntent(parsed, shape);
|
|
98
|
+
const suggestedAbstraction = buildSuggestedAbstraction(intent, parsed);
|
|
99
|
+
const targetFiles = uniqueFiles(shape.files ?? []);
|
|
100
|
+
const storyBlueprint = buildDuplicateQueryStoryBlueprint({
|
|
101
|
+
shape,
|
|
102
|
+
parsed,
|
|
103
|
+
intent,
|
|
104
|
+
suggestedAbstraction,
|
|
105
|
+
targetFiles
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
id: `VP-OPP-DRY-${formatSerial(serialNumber)}`,
|
|
110
|
+
finding_id: 'VP-DRY-001',
|
|
111
|
+
source: 'duplicate_query_shape',
|
|
112
|
+
title: storyBlueprint.title,
|
|
113
|
+
refactoring_intent: intent,
|
|
114
|
+
target_count: targetFiles.length,
|
|
115
|
+
target_files: targetFiles,
|
|
116
|
+
confidence: shape.confidence ?? 'medium',
|
|
117
|
+
priority: shape.gate_effect === 'block' ? 'high' : 'medium',
|
|
118
|
+
suggested_abstraction: suggestedAbstraction,
|
|
119
|
+
evidence_refs: {
|
|
120
|
+
signature: shape.signature,
|
|
121
|
+
model: parsed.model,
|
|
122
|
+
operation: parsed.operation,
|
|
123
|
+
where_keys: parsed.where_keys,
|
|
124
|
+
select_keys: parsed.select_keys,
|
|
125
|
+
occurrence_count: shape.occurrence_count ?? 0,
|
|
126
|
+
file_count: shape.file_count ?? targetFiles.length,
|
|
127
|
+
examples: shape.examples ?? []
|
|
128
|
+
},
|
|
129
|
+
story_blueprint: storyBlueprint
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function buildResponsibilityHotspotOpportunities(hotspots) {
|
|
134
|
+
return hotspots
|
|
135
|
+
.filter((hotspot) => hotspot?.gate_effect !== INFO_GATE_EFFECT)
|
|
136
|
+
.map((hotspot, index) => {
|
|
137
|
+
const targetFiles = uniqueFiles([hotspot.file]);
|
|
138
|
+
const storyBlueprint = buildResponsibilityStoryBlueprint({ hotspot, targetFiles });
|
|
139
|
+
return {
|
|
140
|
+
id: `VP-OPP-ARCH-${formatSerial(index + 1)}`,
|
|
141
|
+
finding_id: 'VP-ARCH-001',
|
|
142
|
+
source: 'responsibility_hotspot',
|
|
143
|
+
title: storyBlueprint.title,
|
|
144
|
+
refactoring_intent: 'responsibility_split',
|
|
145
|
+
target_count: targetFiles.length,
|
|
146
|
+
target_files: targetFiles,
|
|
147
|
+
confidence: hotspot.confidence ?? 'medium',
|
|
148
|
+
priority: hotspot.gate_effect === 'block' ? 'high' : 'medium',
|
|
149
|
+
suggested_abstraction: {
|
|
150
|
+
id: 'split-runtime-responsibilities',
|
|
151
|
+
label: 'runtime責務を境界ごとに分離する',
|
|
152
|
+
target_shape: 'route/action/service/helpers'
|
|
153
|
+
},
|
|
154
|
+
evidence_refs: {
|
|
155
|
+
file: hotspot.file,
|
|
156
|
+
line_count: hotspot.line_count ?? null,
|
|
157
|
+
signals: hotspot.signals ?? [],
|
|
158
|
+
examples: hotspot.examples ?? []
|
|
159
|
+
},
|
|
160
|
+
story_blueprint: storyBlueprint
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildDuplicateQueryStoryBlueprint({ shape, parsed, intent, suggestedAbstraction, targetFiles }) {
|
|
166
|
+
const label = intentLabel(intent);
|
|
167
|
+
const queryLabel = [parsed.model, parsed.operation].filter(Boolean).join('.') || 'Prisma query';
|
|
168
|
+
return {
|
|
169
|
+
title: `${label}の重複query形状を共通化する`,
|
|
170
|
+
summary: `${shape.occurrence_count ?? targetFiles.length}箇所に出ている ${queryLabel} の同一where/select/orderByを、用途が同じか確認したうえで共通境界へ寄せる。`,
|
|
171
|
+
source_finding_id: 'VP-DRY-001',
|
|
172
|
+
refactoring_intent: intent,
|
|
173
|
+
current_behavior: {
|
|
174
|
+
query: queryLabel,
|
|
175
|
+
occurrence_count: shape.occurrence_count ?? 0,
|
|
176
|
+
file_count: shape.file_count ?? targetFiles.length,
|
|
177
|
+
target_files: targetFiles
|
|
178
|
+
},
|
|
179
|
+
behavior_variants: buildQueryBehaviorVariants(parsed),
|
|
180
|
+
suggested_abstraction: suggestedAbstraction,
|
|
181
|
+
invariants: [
|
|
182
|
+
'返却データのshapeを既存呼び出し元ごとに変えない。',
|
|
183
|
+
'where/select/orderBy/take/skip/cursorの意味を共通化前後で一致させる。',
|
|
184
|
+
'用途が異なる重複は無理に統合せず、Story内で別責務として分ける。'
|
|
185
|
+
],
|
|
186
|
+
acceptance_criteria: [
|
|
187
|
+
'重複しているquery形状の用途が同じか、Story上で判断根拠が明示されている。',
|
|
188
|
+
'同じ用途のquery形状はservice/helper/repositoryなど単一の境界に集約されている。',
|
|
189
|
+
'既存の呼び出し元テストまたは型検査で返却shapeの互換性が確認されている。',
|
|
190
|
+
'VibePro診断で対象の重複query形状が減っている。'
|
|
191
|
+
],
|
|
192
|
+
validation_commands: [
|
|
193
|
+
'npm test -- <related tests>',
|
|
194
|
+
'npm run type-check',
|
|
195
|
+
'vibepro diagnose <repo>'
|
|
196
|
+
]
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function buildResponsibilityStoryBlueprint({ hotspot, targetFiles }) {
|
|
201
|
+
return {
|
|
202
|
+
title: '責務混在runtime fileを分離する',
|
|
203
|
+
summary: `${hotspot.file} にDB・認証・検証・外部I/Oなど複数責務が集中しているため、境界をStory化して分離する。`,
|
|
204
|
+
source_finding_id: 'VP-ARCH-001',
|
|
205
|
+
refactoring_intent: 'responsibility_split',
|
|
206
|
+
current_behavior: {
|
|
207
|
+
target_files: targetFiles,
|
|
208
|
+
signals: hotspot.signals ?? [],
|
|
209
|
+
line_count: hotspot.line_count ?? null
|
|
210
|
+
},
|
|
211
|
+
behavior_variants: [
|
|
212
|
+
'route/actionは入力・認可・レスポンス責務を持つ。',
|
|
213
|
+
'service/repositoryはDBアクセスとdomain処理を分離して持つ。',
|
|
214
|
+
'外部I/Oや通知は副作用境界として切り出す。'
|
|
215
|
+
],
|
|
216
|
+
suggested_abstraction: {
|
|
217
|
+
id: 'split-runtime-responsibilities',
|
|
218
|
+
label: 'runtime責務を境界ごとに分離する',
|
|
219
|
+
target_shape: 'route/action/service/helpers'
|
|
220
|
+
},
|
|
221
|
+
invariants: [
|
|
222
|
+
'APIまたはUIから見える入出力を変えない。',
|
|
223
|
+
'認可順序とエラーハンドリングを分離前後で維持する。',
|
|
224
|
+
'副作用を呼び出すタイミングを変えない。'
|
|
225
|
+
],
|
|
226
|
+
acceptance_criteria: [
|
|
227
|
+
'混在していた責務が読み取れる単位へ分離されている。',
|
|
228
|
+
'既存テストまたは型検査で入出力互換性が確認されている。',
|
|
229
|
+
'VibePro診断で責務混在候補の根拠が減っている。'
|
|
230
|
+
],
|
|
231
|
+
validation_commands: [
|
|
232
|
+
'npm test -- <related tests>',
|
|
233
|
+
'npm run type-check',
|
|
234
|
+
'vibepro diagnose <repo>'
|
|
235
|
+
]
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function buildRefactoringImplementationPlan(opportunity, campaign = null, graphContext = null) {
|
|
240
|
+
const targetFiles = campaign?.target_files ?? opportunity.target_files;
|
|
241
|
+
const readFirstFiles = buildRefactoringReadFirstFiles({ targetFiles, opportunity, campaign, graphContext });
|
|
242
|
+
return {
|
|
243
|
+
priority: campaign?.priority ?? opportunity.priority,
|
|
244
|
+
rationale: campaign
|
|
245
|
+
? `${campaign.id} は ${campaign.opportunity_count}件の機会を束ねるStory候補。最初に ${opportunity.id} を確認する。`
|
|
246
|
+
: `${opportunity.finding_id} から ${opportunity.refactoring_intent} としてStory化できる候補。対象は ${opportunity.target_count}ファイル。`,
|
|
247
|
+
read_first_files: readFirstFiles,
|
|
248
|
+
steps: [
|
|
249
|
+
{
|
|
250
|
+
id: 'inventory-current-behavior',
|
|
251
|
+
title: '現在の挙動を棚卸しする',
|
|
252
|
+
detail: '対象ファイルごとにquery条件、返却shape、fallback、例外処理、呼び出し元期待値を確認する。Graphifyの関連ファイルがある場合は先に呼び出し方向と共有hubを確認する。'
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: 'decide-abstraction-boundary',
|
|
256
|
+
title: '共通境界を決める',
|
|
257
|
+
detail: '同じ用途なら共通service/helper/repositoryへ集約する。複数communityに跨る場合は、共通化前にflow単位の責務差分をStory内で分ける。'
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
id: 'replace-call-sites',
|
|
261
|
+
title: '呼び出し元を置き換える',
|
|
262
|
+
detail: '既存の返却shapeを保ったまま、対象箇所を共通境界へ接続する。'
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
id: 'rerun-diagnosis',
|
|
266
|
+
title: '診断を再実行する',
|
|
267
|
+
detail: '型検査・関連テスト・VibePro診断で対象機会が減ったこと、Graphify上の影響範囲外を不用意に変更していないことを確認する。'
|
|
268
|
+
}
|
|
269
|
+
],
|
|
270
|
+
acceptance_criteria: campaign?.story_blueprint?.acceptance_criteria ?? opportunity.story_blueprint.acceptance_criteria,
|
|
271
|
+
pre_fix_briefing: {
|
|
272
|
+
opportunity: {
|
|
273
|
+
id: opportunity.id,
|
|
274
|
+
source: opportunity.source,
|
|
275
|
+
refactoring_intent: opportunity.refactoring_intent,
|
|
276
|
+
rank: opportunity.rank,
|
|
277
|
+
score: opportunity.score,
|
|
278
|
+
suggested_abstraction: opportunity.suggested_abstraction,
|
|
279
|
+
evidence_refs: opportunity.evidence_refs
|
|
280
|
+
},
|
|
281
|
+
campaign: campaign
|
|
282
|
+
? {
|
|
283
|
+
id: campaign.id,
|
|
284
|
+
rank: campaign.rank,
|
|
285
|
+
title: campaign.title,
|
|
286
|
+
opportunity_ids: campaign.opportunity_ids,
|
|
287
|
+
expected_diagnostic_delta: campaign.expected_diagnostic_delta
|
|
288
|
+
}
|
|
289
|
+
: null,
|
|
290
|
+
target_files: targetFiles,
|
|
291
|
+
graph_context: graphContext,
|
|
292
|
+
investigation_scope: buildRefactoringInvestigationScope({ targetFiles, readFirstFiles, graphContext }),
|
|
293
|
+
invariants: campaign?.story_blueprint?.invariants ?? opportunity.story_blueprint.invariants,
|
|
294
|
+
evidence_examples: opportunity.evidence_refs.examples ?? [],
|
|
295
|
+
strategy_options: buildRefactoringStrategyOptions(opportunity, targetFiles, graphContext),
|
|
296
|
+
recommended_strategy: buildRefactoringRecommendedStrategy(opportunity, graphContext)
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function buildRefactoringReadFirstFiles({ targetFiles, opportunity, campaign, graphContext }) {
|
|
302
|
+
const files = [];
|
|
303
|
+
const seen = new Set();
|
|
304
|
+
const add = (file, reason) => {
|
|
305
|
+
if (files.length >= 12 || !file || seen.has(file)) return;
|
|
306
|
+
seen.add(file);
|
|
307
|
+
files.push({ file, reason });
|
|
308
|
+
};
|
|
309
|
+
const graphItems = buildRefactoringGraphReadFirstItems(graphContext);
|
|
310
|
+
const targetBudget = graphItems.length > 0 && (targetFiles?.length ?? 0) > 8
|
|
311
|
+
? Math.max(6, 12 - Math.min(4, graphItems.length))
|
|
312
|
+
: 12;
|
|
313
|
+
const targetReason = campaign
|
|
314
|
+
? `リファクタリングcampaign ${campaign.id} の対象ファイル`
|
|
315
|
+
: `リファクタリング機会 ${opportunity.id} の対象ファイル`;
|
|
316
|
+
for (const file of (targetFiles ?? []).slice(0, targetBudget)) {
|
|
317
|
+
add(file, targetReason);
|
|
318
|
+
}
|
|
319
|
+
for (const item of graphItems) {
|
|
320
|
+
add(item.file, item.reason);
|
|
321
|
+
}
|
|
322
|
+
for (const file of targetFiles ?? []) {
|
|
323
|
+
add(file, targetReason);
|
|
324
|
+
}
|
|
325
|
+
return files;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function buildRefactoringGraphReadFirstItems(graphContext) {
|
|
329
|
+
const items = [];
|
|
330
|
+
const seen = new Set();
|
|
331
|
+
const add = (file, reason) => {
|
|
332
|
+
if (!file || seen.has(file)) return;
|
|
333
|
+
seen.add(file);
|
|
334
|
+
items.push({ file, reason });
|
|
335
|
+
};
|
|
336
|
+
for (const file of graphContext?.related_files ?? []) {
|
|
337
|
+
add(file, 'Graphifyで対象ファイルと直接つながる周辺ファイル');
|
|
338
|
+
}
|
|
339
|
+
for (const hub of graphContext?.hub_nodes ?? []) {
|
|
340
|
+
add(hub.source_file, `Graphify hub: ${hub.label ?? hub.id} / degree=${hub.degree ?? 0}`);
|
|
341
|
+
}
|
|
342
|
+
return items;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function buildRefactoringInvestigationScope({ targetFiles, readFirstFiles, graphContext }) {
|
|
346
|
+
return {
|
|
347
|
+
target_files: targetFiles ?? [],
|
|
348
|
+
read_first_files: readFirstFiles.map((item) => item.file),
|
|
349
|
+
graph_matched_files: graphContext?.matched_files ?? [],
|
|
350
|
+
graph_unmatched_files: graphContext?.unmatched_files ?? [],
|
|
351
|
+
related_files: graphContext?.related_files ?? [],
|
|
352
|
+
hub_nodes: graphContext?.hub_nodes ?? [],
|
|
353
|
+
affected_communities: graphContext?.affected_communities ?? [],
|
|
354
|
+
community_span: graphContext?.community_span ?? 0,
|
|
355
|
+
cross_community: Boolean(graphContext?.cross_community),
|
|
356
|
+
guidance: buildRefactoringGraphGuidance(graphContext)
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function buildRefactoringGraphGuidance(graphContext) {
|
|
361
|
+
if (!graphContext || (graphContext.matched_file_count ?? 0) === 0) {
|
|
362
|
+
return 'Graphifyで対象ファイルに対応するnodeが見つからないため、対象ファイルと静的import/呼び出し元を手動で確認する。';
|
|
363
|
+
}
|
|
364
|
+
if (graphContext.cross_community) {
|
|
365
|
+
return '複数communityに跨るため、先にflowごとの責務差分を確認し、共通化は安定した境界に限定する。';
|
|
366
|
+
}
|
|
367
|
+
return '同一community内の影響が中心のため、hub/related fileを先に読んで共有境界を決める。';
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function buildRefactoringStrategyOptions(opportunity, targetFiles, graphContext = null) {
|
|
371
|
+
const graphCaution = graphContext?.cross_community
|
|
372
|
+
? 'Graphify上で複数communityに跨るため、flow差分を確認してから共通化する必要がある'
|
|
373
|
+
: null;
|
|
374
|
+
if (opportunity.source === 'responsibility_hotspot') {
|
|
375
|
+
return [
|
|
376
|
+
{
|
|
377
|
+
id: 'split-runtime-boundaries',
|
|
378
|
+
label: '方針A: runtime責務をroute/action/service/helperへ分離する',
|
|
379
|
+
target_count: targetFiles.length,
|
|
380
|
+
candidate_files: targetFiles,
|
|
381
|
+
benefits: ['認可、DB、検証、外部I/Oの責務境界を読みやすくできる', '副作用の順序をレビューしやすくなる'],
|
|
382
|
+
cautions: ['入出力と副作用タイミングを先に固定する必要がある', graphCaution].filter(Boolean)
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
id: 'extract-side-effect-boundary',
|
|
386
|
+
label: '方針B: 外部I/Oや通知など副作用境界から切り出す',
|
|
387
|
+
target_count: targetFiles.length,
|
|
388
|
+
candidate_files: targetFiles,
|
|
389
|
+
benefits: ['大きなファイルを段階的に小さくできる', 'テストしづらい副作用を隔離できる'],
|
|
390
|
+
cautions: ['DB更新やレスポンス整形との順序を変えない確認が必要', graphCaution].filter(Boolean)
|
|
391
|
+
}
|
|
392
|
+
];
|
|
393
|
+
}
|
|
394
|
+
return [
|
|
395
|
+
{
|
|
396
|
+
id: 'extract-shared-boundary',
|
|
397
|
+
label: '方針A: 同じ用途の処理を共通境界へ抽出する',
|
|
398
|
+
target_count: targetFiles.length,
|
|
399
|
+
candidate_files: targetFiles,
|
|
400
|
+
benefits: ['重複query形状を直接減らせる', '挙動変更を一箇所で検証しやすい'],
|
|
401
|
+
cautions: ['用途が違う重複を誤って統合しない確認が必要', graphCaution].filter(Boolean)
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
id: 'separate-behavior-variants',
|
|
405
|
+
label: '方針B: 用途差分を明示して責務を分ける',
|
|
406
|
+
target_count: targetFiles.length,
|
|
407
|
+
candidate_files: targetFiles,
|
|
408
|
+
benefits: ['暗黙の差分をStoryに残せる', '無理な共通化による回帰を避けやすい'],
|
|
409
|
+
cautions: ['重複削減より責務明確化を優先する判断になる', graphCaution].filter(Boolean)
|
|
410
|
+
}
|
|
411
|
+
];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function buildRefactoringRecommendedStrategy(opportunity, graphContext = null) {
|
|
415
|
+
if (graphContext?.cross_community) {
|
|
416
|
+
return {
|
|
417
|
+
id: 'split-by-graph-community',
|
|
418
|
+
reason: 'Graphify上で複数communityに跨るため、先にflow単位の責務差分を分け、共通化は安定した境界に限定する。'
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
if (opportunity.source === 'responsibility_hotspot') {
|
|
422
|
+
return {
|
|
423
|
+
id: 'split-runtime-boundaries',
|
|
424
|
+
reason: '責務混在候補は重複削減より先に、認可、DB、検証、外部I/Oの境界を固定する価値が高い。'
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
id: 'extract-shared-boundary',
|
|
429
|
+
reason: '同一query形状が複数ファイルで繰り返されており、まず用途一致を確認して共通境界化する価値が高い。'
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function rankRefactoringOpportunities(opportunities, evidence) {
|
|
434
|
+
return opportunities
|
|
435
|
+
.map((opportunity) => {
|
|
436
|
+
const score = scoreRefactoringOpportunity(opportunity, evidence);
|
|
437
|
+
return {
|
|
438
|
+
...opportunity,
|
|
439
|
+
priority: score.total >= 75 ? 'high' : score.total >= 40 ? opportunity.priority : 'low',
|
|
440
|
+
score,
|
|
441
|
+
priority_reasons: score.reasons
|
|
442
|
+
};
|
|
443
|
+
})
|
|
444
|
+
.sort((a, b) => b.score.total - a.score.total || compareOpportunities(a, b))
|
|
445
|
+
.map((opportunity, index) => ({
|
|
446
|
+
...opportunity,
|
|
447
|
+
rank: index + 1
|
|
448
|
+
}));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function scoreRefactoringOpportunity(opportunity, evidence) {
|
|
452
|
+
const reasons = [];
|
|
453
|
+
const targetFiles = opportunity.target_files ?? [];
|
|
454
|
+
const occurrenceCount = opportunity.evidence_refs?.occurrence_count ?? opportunity.target_count ?? 0;
|
|
455
|
+
const securityScore = scoreSecurityProximity(opportunity, targetFiles, reasons);
|
|
456
|
+
const blastRadiusScore = Math.min(25, (targetFiles.length * 4) + (occurrenceCount * 2));
|
|
457
|
+
if (blastRadiusScore >= 12) reasons.push(`blast_radius:${blastRadiusScore}`);
|
|
458
|
+
const confidenceScore = { high: 15, medium: 10, low: 5 }[opportunity.confidence] ?? 5;
|
|
459
|
+
reasons.push(`confidence:${opportunity.confidence ?? 'unknown'}`);
|
|
460
|
+
const storyFitScore = scoreStoryFit(opportunity, evidence?.story, reasons);
|
|
461
|
+
const sourceScore = opportunity.source === 'duplicate_query_shape' ? 8 : 5;
|
|
462
|
+
const total = securityScore + blastRadiusScore + confidenceScore + storyFitScore + sourceScore;
|
|
463
|
+
return {
|
|
464
|
+
total,
|
|
465
|
+
components: {
|
|
466
|
+
security_proximity: securityScore,
|
|
467
|
+
blast_radius: blastRadiusScore,
|
|
468
|
+
confidence: confidenceScore,
|
|
469
|
+
story_fit: storyFitScore,
|
|
470
|
+
source: sourceScore
|
|
471
|
+
},
|
|
472
|
+
reasons
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function scoreSecurityProximity(opportunity, targetFiles, reasons) {
|
|
477
|
+
const haystack = [
|
|
478
|
+
opportunity.refactoring_intent,
|
|
479
|
+
opportunity.title,
|
|
480
|
+
...(targetFiles ?? [])
|
|
481
|
+
].join(' ').toLowerCase();
|
|
482
|
+
if (opportunity.refactoring_intent === 'authorization_boundary') {
|
|
483
|
+
reasons.push('security_proximity:authorization_boundary');
|
|
484
|
+
return 30;
|
|
485
|
+
}
|
|
486
|
+
if (/(auth|session|user|account|identity|middleware|permission|role)/.test(haystack)) {
|
|
487
|
+
reasons.push('security_proximity:auth_or_identity');
|
|
488
|
+
return 25;
|
|
489
|
+
}
|
|
490
|
+
if (/(billing|subscription|stripe|payment|invoice|webhook)/.test(haystack)) {
|
|
491
|
+
reasons.push('security_proximity:billing_or_webhook');
|
|
492
|
+
return 22;
|
|
493
|
+
}
|
|
494
|
+
if (/(api\/|route\.ts|route\.js|server)/.test(haystack)) {
|
|
495
|
+
reasons.push('security_proximity:runtime_boundary');
|
|
496
|
+
return 15;
|
|
497
|
+
}
|
|
498
|
+
return 5;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function scoreStoryFit(opportunity, story, reasons) {
|
|
502
|
+
const patterns = extractStoryCoveragePatterns(story);
|
|
503
|
+
if (patterns.length === 0) return 5;
|
|
504
|
+
const targetFiles = opportunity.target_files ?? [];
|
|
505
|
+
const matched = targetFiles.filter((file) => patterns.some((pattern) => matchesStoryPattern(file, pattern)));
|
|
506
|
+
if (matched.length === 0) return 0;
|
|
507
|
+
reasons.push(`story_fit:${matched.length}/${targetFiles.length}`);
|
|
508
|
+
return Math.min(15, 5 + matched.length * 3);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function extractStoryCoveragePatterns(story) {
|
|
512
|
+
const rawPatterns = [
|
|
513
|
+
...(story?.coverage_patterns ?? []),
|
|
514
|
+
...(story?.coveragePatterns ?? []),
|
|
515
|
+
...(story?.derived?.coverage_patterns ?? []),
|
|
516
|
+
...(story?.derived?.coveragePatterns ?? [])
|
|
517
|
+
];
|
|
518
|
+
return rawPatterns
|
|
519
|
+
.map((pattern) => typeof pattern === 'string' ? pattern : pattern?.path ?? pattern?.pattern ?? null)
|
|
520
|
+
.filter(Boolean);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function matchesStoryPattern(file, pattern) {
|
|
524
|
+
const normalizedFile = normalizePath(file);
|
|
525
|
+
const normalizedPattern = normalizePath(pattern)
|
|
526
|
+
.replace(/\*\*\/?/g, '')
|
|
527
|
+
.replace(/\*/g, '');
|
|
528
|
+
if (!normalizedPattern) return false;
|
|
529
|
+
return normalizedFile.includes(normalizedPattern.replace(/\/$/, ''));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function buildCampaignKey(opportunity) {
|
|
533
|
+
return `${opportunity.refactoring_intent}:${resolveCampaignDomain(opportunity)}`;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function resolveCampaignDomain(opportunity) {
|
|
537
|
+
const haystack = [
|
|
538
|
+
opportunity.title,
|
|
539
|
+
opportunity.refactoring_intent,
|
|
540
|
+
...(opportunity.target_files ?? [])
|
|
541
|
+
].join(' ').toLowerCase();
|
|
542
|
+
if (/(auth|session|identity|user|account|profile|member)/.test(haystack)) return 'identity';
|
|
543
|
+
if (/(billing|subscription|stripe|payment|invoice|customer)/.test(haystack)) return 'billing';
|
|
544
|
+
if (/(permission|role|owner|tenant|workspace|organization|policy|access)/.test(haystack)) return 'authorization';
|
|
545
|
+
if (/(webhook|notification|event|integration|audit)/.test(haystack)) return 'integration';
|
|
546
|
+
if (/(api\/|route\.ts|route\.js)/.test(haystack)) return 'api';
|
|
547
|
+
return 'application';
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function buildRefactoringCampaign(opportunities, serialNumber) {
|
|
551
|
+
const sorted = [...opportunities].sort((a, b) => b.score.total - a.score.total || a.id.localeCompare(b.id));
|
|
552
|
+
const primary = sorted[0];
|
|
553
|
+
const targetFiles = uniqueFiles(sorted.flatMap((opportunity) => opportunity.target_files ?? []));
|
|
554
|
+
const scoreTotal = Math.round(sorted.reduce((sum, opportunity) => sum + opportunity.score.total, 0) / sorted.length);
|
|
555
|
+
const priority = scoreTotal >= 75 ? 'high' : scoreTotal >= 40 ? 'medium' : 'low';
|
|
556
|
+
const storyBlueprint = buildCampaignStoryBlueprint({ primary, opportunities: sorted, targetFiles });
|
|
557
|
+
const graphContext = mergeRefactoringGraphContexts(sorted.map((opportunity) => opportunity.graph_context));
|
|
558
|
+
return {
|
|
559
|
+
id: `VP-CAMPAIGN-REF-${formatSerial(serialNumber)}`,
|
|
560
|
+
title: storyBlueprint.title,
|
|
561
|
+
refactoring_intent: primary.refactoring_intent,
|
|
562
|
+
domain: resolveCampaignDomain(primary),
|
|
563
|
+
priority,
|
|
564
|
+
score: {
|
|
565
|
+
total: scoreTotal,
|
|
566
|
+
top_opportunity_score: primary.score.total
|
|
567
|
+
},
|
|
568
|
+
opportunity_count: sorted.length,
|
|
569
|
+
opportunity_ids: sorted.map((opportunity) => opportunity.id),
|
|
570
|
+
finding_ids: [...new Set(sorted.map((opportunity) => opportunity.finding_id))],
|
|
571
|
+
target_count: targetFiles.length,
|
|
572
|
+
target_files: targetFiles.slice(0, 20),
|
|
573
|
+
recommended_first_opportunity_id: primary.id,
|
|
574
|
+
expected_diagnostic_delta: {
|
|
575
|
+
duplicate_query_shapes: sorted.filter((opportunity) => opportunity.finding_id === 'VP-DRY-001').length,
|
|
576
|
+
responsibility_hotspots: sorted.filter((opportunity) => opportunity.finding_id === 'VP-ARCH-001').length
|
|
577
|
+
},
|
|
578
|
+
priority_reasons: uniqueFiles(sorted.flatMap((opportunity) => opportunity.priority_reasons ?? [])).slice(0, 10),
|
|
579
|
+
graph_context: graphContext,
|
|
580
|
+
story_blueprint: storyBlueprint
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function mergeRefactoringGraphContexts(contexts) {
|
|
585
|
+
const items = contexts.filter(Boolean);
|
|
586
|
+
if (items.length === 0) return null;
|
|
587
|
+
const matchedFiles = uniqueFiles(items.flatMap((context) => context.matched_files ?? []));
|
|
588
|
+
const unmatchedFiles = uniqueFiles(items.flatMap((context) => context.unmatched_files ?? []))
|
|
589
|
+
.filter((file) => !matchedFiles.includes(file));
|
|
590
|
+
const relatedFiles = uniqueFiles(items.flatMap((context) => context.related_files ?? []));
|
|
591
|
+
const communities = mergeAffectedCommunities(items.flatMap((context) => context.affected_communities ?? []));
|
|
592
|
+
return {
|
|
593
|
+
matched_route_count: items.reduce((sum, context) => sum + (context.matched_route_count ?? 0), 0),
|
|
594
|
+
target_file_count: uniqueFiles(items.flatMap((context) => [
|
|
595
|
+
...(context.matched_files ?? []),
|
|
596
|
+
...(context.unmatched_files ?? [])
|
|
597
|
+
])).length,
|
|
598
|
+
matched_file_count: matchedFiles.length,
|
|
599
|
+
matched_files: matchedFiles,
|
|
600
|
+
unmatched_files: unmatchedFiles,
|
|
601
|
+
matched_node_count: items.reduce((sum, context) => sum + (context.matched_node_count ?? 0), 0),
|
|
602
|
+
affected_communities: communities,
|
|
603
|
+
hub_nodes: mergeHubNodes(items.flatMap((context) => context.hub_nodes ?? [])),
|
|
604
|
+
related_files: relatedFiles.slice(0, 8),
|
|
605
|
+
related_edge_count: items.reduce((sum, context) => sum + (context.related_edge_count ?? 0), 0),
|
|
606
|
+
impact_score: Math.max(...items.map((context) => context.impact_score ?? 0)),
|
|
607
|
+
community_span: communities.length,
|
|
608
|
+
cross_community: communities.length > 1
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function mergeAffectedCommunities(communities) {
|
|
613
|
+
const byId = new Map();
|
|
614
|
+
for (const community of communities) {
|
|
615
|
+
if (!community) continue;
|
|
616
|
+
const key = String(community.id);
|
|
617
|
+
const item = byId.get(key) ?? {
|
|
618
|
+
id: community.id,
|
|
619
|
+
route_count: 0,
|
|
620
|
+
file_count: 0,
|
|
621
|
+
node_count: 0,
|
|
622
|
+
edge_count: 0
|
|
623
|
+
};
|
|
624
|
+
item.route_count += community.route_count ?? 0;
|
|
625
|
+
item.file_count += community.file_count ?? 0;
|
|
626
|
+
item.node_count += community.node_count ?? 0;
|
|
627
|
+
item.edge_count += community.edge_count ?? 0;
|
|
628
|
+
byId.set(key, item);
|
|
629
|
+
}
|
|
630
|
+
return [...byId.values()]
|
|
631
|
+
.sort((a, b) => b.route_count - a.route_count || b.file_count - a.file_count || b.node_count - a.node_count || String(a.id).localeCompare(String(b.id)));
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function mergeHubNodes(hubNodes) {
|
|
635
|
+
const byId = new Map();
|
|
636
|
+
for (const node of hubNodes) {
|
|
637
|
+
if (!node?.id) continue;
|
|
638
|
+
const previous = byId.get(node.id);
|
|
639
|
+
if (!previous || (node.degree ?? 0) > (previous.degree ?? 0)) byId.set(node.id, node);
|
|
640
|
+
}
|
|
641
|
+
return [...byId.values()]
|
|
642
|
+
.sort((a, b) => (b.degree ?? 0) - (a.degree ?? 0) || a.id.localeCompare(b.id))
|
|
643
|
+
.slice(0, 5);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function buildCampaignStoryBlueprint({ primary, opportunities, targetFiles }) {
|
|
647
|
+
const title = `${intentLabel(primary.refactoring_intent)} campaignをStory化する`;
|
|
648
|
+
return {
|
|
649
|
+
title,
|
|
650
|
+
summary: `${opportunities.length}件の ${primary.refactoring_intent} 機会を、診断で効果確認できるStory単位に束ねる。最初の対象は ${primary.id}。`,
|
|
651
|
+
source_opportunity_ids: opportunities.map((opportunity) => opportunity.id),
|
|
652
|
+
source_finding_ids: [...new Set(opportunities.map((opportunity) => opportunity.finding_id))],
|
|
653
|
+
refactoring_intent: primary.refactoring_intent,
|
|
654
|
+
target_files: targetFiles.slice(0, 20),
|
|
655
|
+
recommended_sequence: opportunities.slice(0, 5).map((opportunity, index) => ({
|
|
656
|
+
order: index + 1,
|
|
657
|
+
opportunity_id: opportunity.id,
|
|
658
|
+
title: opportunity.title,
|
|
659
|
+
reason: opportunity.priority_reasons?.slice(0, 3) ?? []
|
|
660
|
+
})),
|
|
661
|
+
invariants: uniqueFiles(opportunities.flatMap((opportunity) => opportunity.story_blueprint?.invariants ?? [])).slice(0, 8),
|
|
662
|
+
acceptance_criteria: [
|
|
663
|
+
'campaign内の機会がStory単位として実装順に並んでいる。',
|
|
664
|
+
'最初に直す機会と後続に回す機会の判断根拠がscore/reasonで説明できる。',
|
|
665
|
+
'修正後のVibePro診断で対象findingまたはopportunityの件数差分を確認できる。',
|
|
666
|
+
...uniqueFiles(opportunities.flatMap((opportunity) => opportunity.story_blueprint?.acceptance_criteria ?? [])).slice(0, 4)
|
|
667
|
+
],
|
|
668
|
+
validation_commands: [
|
|
669
|
+
'npm test -- <related tests>',
|
|
670
|
+
'npm run type-check',
|
|
671
|
+
'vibepro diagnose <repo>'
|
|
672
|
+
]
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function findCampaignForOpportunity(campaigns, opportunityId) {
|
|
677
|
+
return campaigns.find((campaign) => campaign.opportunity_ids?.includes(opportunityId)) ?? null;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function parseQuerySignature(signature = '') {
|
|
681
|
+
const [operationPart = '', ...parts] = String(signature).split('|');
|
|
682
|
+
const [rawModel = '', operation = 'unknown'] = operationPart.split('.');
|
|
683
|
+
const clauses = Object.fromEntries(parts.map((part) => {
|
|
684
|
+
const separatorIndex = part.indexOf(':');
|
|
685
|
+
if (separatorIndex === -1) return [part, ''];
|
|
686
|
+
return [part.slice(0, separatorIndex), part.slice(separatorIndex + 1)];
|
|
687
|
+
}));
|
|
688
|
+
return {
|
|
689
|
+
model: normalizeModelName(rawModel),
|
|
690
|
+
operation,
|
|
691
|
+
where_keys: splitKeys(clauses.where),
|
|
692
|
+
select_keys: splitKeys(clauses.select),
|
|
693
|
+
order_keys: splitKeys(clauses.order),
|
|
694
|
+
top_keys: splitKeys(clauses.top),
|
|
695
|
+
raw_signature: signature
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function normalizeModelName(model) {
|
|
700
|
+
return String(model ?? '')
|
|
701
|
+
.replace(/^t_/, '')
|
|
702
|
+
.replace(/^prisma\./, '')
|
|
703
|
+
.replace(/^db\./, '');
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function splitKeys(value) {
|
|
707
|
+
if (!value || value === '-') return [];
|
|
708
|
+
return String(value).split(',').map((item) => item.trim()).filter(Boolean);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function classifyQueryIntent(parsed, shape) {
|
|
712
|
+
const haystack = [
|
|
713
|
+
parsed.model,
|
|
714
|
+
parsed.operation,
|
|
715
|
+
parsed.raw_signature,
|
|
716
|
+
...(shape.files ?? [])
|
|
717
|
+
].join(' ').toLowerCase();
|
|
718
|
+
if (/(user|account|profile|identity|member)/.test(haystack) && /(auth|email|login|session|credential|provider|nextauth)/.test(haystack)) {
|
|
719
|
+
return 'identity_resolution';
|
|
720
|
+
}
|
|
721
|
+
if (/(subscription|billing|stripe|invoice|plan|customer|payment)/.test(haystack)) {
|
|
722
|
+
return 'subscription_state_sync';
|
|
723
|
+
}
|
|
724
|
+
if (/(tenant|workspace|organization|permission|role|owner|policy|access)/.test(haystack)) {
|
|
725
|
+
return 'authorization_boundary';
|
|
726
|
+
}
|
|
727
|
+
if (/(webhook|notification|event|audit|log|integration)/.test(haystack)) {
|
|
728
|
+
return 'integration_event_policy';
|
|
729
|
+
}
|
|
730
|
+
return 'query_policy';
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function buildSuggestedAbstraction(intent, parsed) {
|
|
734
|
+
const labels = {
|
|
735
|
+
identity_resolution: 'identity resolver/service',
|
|
736
|
+
subscription_state_sync: 'subscription state sync helper',
|
|
737
|
+
authorization_boundary: 'authorization boundary service',
|
|
738
|
+
integration_event_policy: 'integration event repository/helper',
|
|
739
|
+
query_policy: 'shared query policy helper'
|
|
740
|
+
};
|
|
741
|
+
return {
|
|
742
|
+
id: intent,
|
|
743
|
+
label: labels[intent] ?? 'shared data access helper',
|
|
744
|
+
target_shape: parsed.model
|
|
745
|
+
? `${parsed.model} ${parsed.operation} query boundary`
|
|
746
|
+
: 'shared query boundary'
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function buildQueryBehaviorVariants(parsed) {
|
|
751
|
+
return [
|
|
752
|
+
`where keys: ${parsed.where_keys.join(', ') || '-'}`,
|
|
753
|
+
`select keys: ${parsed.select_keys.join(', ') || '-'}`,
|
|
754
|
+
`order keys: ${parsed.order_keys.join(', ') || '-'}`
|
|
755
|
+
];
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function intentLabel(intent) {
|
|
759
|
+
const labels = {
|
|
760
|
+
identity_resolution: 'identity resolution',
|
|
761
|
+
subscription_state_sync: 'subscription state sync',
|
|
762
|
+
authorization_boundary: 'authorization boundary',
|
|
763
|
+
integration_event_policy: 'integration event policy',
|
|
764
|
+
query_policy: 'DB query policy',
|
|
765
|
+
responsibility_split: 'responsibility split'
|
|
766
|
+
};
|
|
767
|
+
return labels[intent] ?? 'shared data access';
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function compareOpportunities(a, b) {
|
|
771
|
+
return priorityRank(a.priority) - priorityRank(b.priority)
|
|
772
|
+
|| confidenceRank(a.confidence) - confidenceRank(b.confidence)
|
|
773
|
+
|| b.target_count - a.target_count
|
|
774
|
+
|| a.id.localeCompare(b.id);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function priorityRank(priority) {
|
|
778
|
+
const ranks = { high: 0, medium: 1, low: 2 };
|
|
779
|
+
return ranks[priority] ?? 3;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function confidenceRank(confidence) {
|
|
783
|
+
const ranks = { high: 0, medium: 1, low: 2 };
|
|
784
|
+
return ranks[confidence] ?? 3;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function formatSerial(value) {
|
|
788
|
+
return String(value).padStart(3, '0');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function uniqueFiles(files) {
|
|
792
|
+
return [...new Set((files ?? []).filter(Boolean))];
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function normalizePath(file) {
|
|
796
|
+
return String(file ?? '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
797
|
+
}
|