@controlfront/detect 0.0.1
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/bin/cfb.js +202 -0
- package/package.json +64 -0
- package/src/commands/baseline.js +198 -0
- package/src/commands/init.js +309 -0
- package/src/commands/login.js +71 -0
- package/src/commands/logout.js +44 -0
- package/src/commands/scan.js +1547 -0
- package/src/commands/snapshot.js +191 -0
- package/src/commands/sync.js +127 -0
- package/src/config/baseUrl.js +49 -0
- package/src/data/tailwind-core-spec.js +149 -0
- package/src/engine/runRules.js +210 -0
- package/src/lib/collectDeclaredTokensAuto.js +67 -0
- package/src/lib/collectTokenMatches.js +330 -0
- package/src/lib/collectTokenMatches.js.regex +252 -0
- package/src/lib/loadRules.js +73 -0
- package/src/rules/core/no-hardcoded-colors.js +28 -0
- package/src/rules/core/no-hardcoded-spacing.js +29 -0
- package/src/rules/core/no-inline-styles.js +28 -0
- package/src/utils/authorId.js +106 -0
- package/src/utils/buildAIContributions.js +224 -0
- package/src/utils/buildBlameData.js +388 -0
- package/src/utils/buildDeclaredCssVars.js +185 -0
- package/src/utils/buildDeclaredJson.js +214 -0
- package/src/utils/buildFileChanges.js +372 -0
- package/src/utils/buildRuntimeUsage.js +337 -0
- package/src/utils/detectDeclaredDrift.js +59 -0
- package/src/utils/extractImports.js +178 -0
- package/src/utils/fileExtensions.js +65 -0
- package/src/utils/generateInsights.js +332 -0
- package/src/utils/getAllFiles.js +63 -0
- package/src/utils/getCommitMetaData.js +102 -0
- package/src/utils/getLine.js +14 -0
- package/src/utils/resolveProjectForFolder/index.js +47 -0
- package/src/utils/twClassify.js +138 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buildFileChanges.js
|
|
3
|
+
*
|
|
4
|
+
* Produce per-changed-file "file change rows" from the current snapshot's enriched changed_files.
|
|
5
|
+
*
|
|
6
|
+
* This is intentionally a PURE transform:
|
|
7
|
+
* - It does not read DB state.
|
|
8
|
+
* - It does not attempt to compute cross-commit sequencing (prev_idx/deltas).
|
|
9
|
+
* Those can be computed later server-side (window functions) or in report queries.
|
|
10
|
+
*
|
|
11
|
+
* Output shape matches the `public.file_changes` table columns closely.
|
|
12
|
+
*
|
|
13
|
+
* FIELD SEMANTICS:
|
|
14
|
+
* - idx: Per-commit index (0-based), stable within this commit's changed files
|
|
15
|
+
* - prev_idx: Cross-commit sequence (set server-side via window function)
|
|
16
|
+
* - lines_changed: Actual changed lines (from blame or diff stats)
|
|
17
|
+
* - change_size: Total magnitude (insertions + deletions)
|
|
18
|
+
* - insertions/deletions: Raw git diff stats
|
|
19
|
+
* - author_id: Current commit author (who is touching the file)
|
|
20
|
+
* - prev_top_author_id: Previous top author (whose code is being touched)
|
|
21
|
+
* - is_overwrite: True when prev_top_author_id differs from author_id
|
|
22
|
+
*
|
|
23
|
+
* COMPUTED POST-INSERT (not in this transform):
|
|
24
|
+
* - changes_last_7_days, changes_last_30_days
|
|
25
|
+
* - unique_authors_last_30_days
|
|
26
|
+
* - days_since_last_change
|
|
27
|
+
* - avg_change_size_last_26_weeks
|
|
28
|
+
* - avg_weeks_between_changes
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
function toIsoDate(value) {
|
|
32
|
+
if (!value) return null;
|
|
33
|
+
const d = new Date(value);
|
|
34
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
35
|
+
return d.toISOString();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function pickPath(cf) {
|
|
39
|
+
return (
|
|
40
|
+
(cf && typeof cf.path === "string" && cf.path) ||
|
|
41
|
+
(cf && typeof cf.file === "string" && cf.file) ||
|
|
42
|
+
(cf && typeof cf.to === "string" && cf.to) ||
|
|
43
|
+
(cf && typeof cf.from === "string" && cf.from) ||
|
|
44
|
+
null
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function safeNumber(v) {
|
|
49
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
50
|
+
if (typeof v === "string") {
|
|
51
|
+
const s = v.trim();
|
|
52
|
+
if (!s) return null;
|
|
53
|
+
const n = Number(s);
|
|
54
|
+
return Number.isFinite(n) ? n : null;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function safePercentage(v) {
|
|
60
|
+
const n = safeNumber(v);
|
|
61
|
+
if (n === null) return null;
|
|
62
|
+
// Clamp to valid percentage range
|
|
63
|
+
return Math.max(0, Math.min(100, n));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function inferFileClass(path) {
|
|
67
|
+
if (!path || typeof path !== "string") return null;
|
|
68
|
+
const p = path.toLowerCase();
|
|
69
|
+
|
|
70
|
+
if (p.endsWith(".md") || p.endsWith(".mdx")) return "docs";
|
|
71
|
+
if (
|
|
72
|
+
p.includes("design-tokens") ||
|
|
73
|
+
p.includes("/tokens/") ||
|
|
74
|
+
p.endsWith(".tokens.json")
|
|
75
|
+
)
|
|
76
|
+
return "tokens";
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
p.endsWith(".json") ||
|
|
80
|
+
p.endsWith(".yaml") ||
|
|
81
|
+
p.endsWith(".yml") ||
|
|
82
|
+
p.endsWith(".toml") ||
|
|
83
|
+
p.endsWith(".ini") ||
|
|
84
|
+
p.endsWith(".lock") ||
|
|
85
|
+
p.endsWith("package.json") ||
|
|
86
|
+
p.endsWith("tsconfig.json") ||
|
|
87
|
+
p.endsWith("eslint.config.js") ||
|
|
88
|
+
p.endsWith(".eslintrc") ||
|
|
89
|
+
p.endsWith(".eslintrc.js") ||
|
|
90
|
+
p.endsWith(".eslintrc.json") ||
|
|
91
|
+
p.endsWith(".prettierrc") ||
|
|
92
|
+
p.endsWith(".prettierrc.json") ||
|
|
93
|
+
p.endsWith(".prettierrc.js")
|
|
94
|
+
) {
|
|
95
|
+
return "config";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (p.endsWith(".tsx") || p.endsWith(".jsx")) return "ui";
|
|
99
|
+
if (p.endsWith(".ts") || p.endsWith(".js")) return "code";
|
|
100
|
+
|
|
101
|
+
if (
|
|
102
|
+
p.endsWith(".css") ||
|
|
103
|
+
p.endsWith(".scss") ||
|
|
104
|
+
p.endsWith(".sass") ||
|
|
105
|
+
p.endsWith(".less") ||
|
|
106
|
+
p.endsWith(".styl")
|
|
107
|
+
) {
|
|
108
|
+
return "style";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function pickDiffStats(cf) {
|
|
115
|
+
// Support a few possible shapes coming from enrichment.
|
|
116
|
+
const ds = cf?.diff_stats || cf?.diffStats || cf?.numstat || null;
|
|
117
|
+
const insertions = safeNumber(ds?.insertions ?? ds?.added ?? ds?.ins ?? null);
|
|
118
|
+
const deletions = safeNumber(ds?.deletions ?? ds?.removed ?? ds?.del ?? null);
|
|
119
|
+
|
|
120
|
+
// numstat can emit '-' for binary; represent that as nulls and diffStatsOk=false.
|
|
121
|
+
const binary =
|
|
122
|
+
ds?.insertions === "-" ||
|
|
123
|
+
ds?.deletions === "-" ||
|
|
124
|
+
ds?.added === "-" ||
|
|
125
|
+
ds?.removed === "-";
|
|
126
|
+
|
|
127
|
+
const diffStatsOk = !binary && (insertions !== null || deletions !== null);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
insertions,
|
|
131
|
+
deletions,
|
|
132
|
+
diffStatsOk,
|
|
133
|
+
changeSize:
|
|
134
|
+
insertions !== null || deletions !== null
|
|
135
|
+
? (insertions || 0) + (deletions || 0)
|
|
136
|
+
: null,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function computeConfidence({
|
|
141
|
+
blameCoveragePct,
|
|
142
|
+
diffStatsOk,
|
|
143
|
+
linesChanged,
|
|
144
|
+
status,
|
|
145
|
+
isOverwrite,
|
|
146
|
+
prevTopAuthorPct,
|
|
147
|
+
}) {
|
|
148
|
+
// Deterministic confidence model derived from a small set of observable signals.
|
|
149
|
+
// Intended meaning: "How reliable are the attribution + magnitude signals for this row?"
|
|
150
|
+
//
|
|
151
|
+
// Signals:
|
|
152
|
+
// - blameCoveragePct (primary)
|
|
153
|
+
// - diffStatsOk (supports magnitude, esp. config/lockfiles)
|
|
154
|
+
// - linesChanged (guards against M + 0)
|
|
155
|
+
// - overwrite strength (only when isOverwrite): depends on blame coverage + prev top author dominance
|
|
156
|
+
//
|
|
157
|
+
// RETURNS: float 0.0..1.0 (matches schema: confidence real)
|
|
158
|
+
|
|
159
|
+
const bc = safeNumber(blameCoveragePct); // accepts numeric-ish strings like "40.1"
|
|
160
|
+
const topPct = safeNumber(prevTopAuthorPct);
|
|
161
|
+
const lc = safeNumber(linesChanged);
|
|
162
|
+
|
|
163
|
+
// New files: attribution to commit author is reliable, but overwrite/prior-author signals are N/A.
|
|
164
|
+
if (status === "A") {
|
|
165
|
+
return 0.7;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let c = 0;
|
|
169
|
+
|
|
170
|
+
// blame coverage contribution (0..0.75)
|
|
171
|
+
if (bc === null) c += 0.05;
|
|
172
|
+
else if (bc >= 95) c += 0.75;
|
|
173
|
+
else if (bc >= 75) c += 0.6;
|
|
174
|
+
else if (bc >= 50) c += 0.45;
|
|
175
|
+
else if (bc > 0) c += 0.25;
|
|
176
|
+
|
|
177
|
+
// diff stats contribution (0..0.2)
|
|
178
|
+
if (diffStatsOk) c += 0.2;
|
|
179
|
+
|
|
180
|
+
// non-zero magnitude contribution (0..0.1)
|
|
181
|
+
if (lc !== null && lc > 0) c += 0.1;
|
|
182
|
+
|
|
183
|
+
// overwrite strength contribution (0..0.15)
|
|
184
|
+
// We only award this when we can actually judge overwrite with meaningful blame coverage.
|
|
185
|
+
// Strength increases when the previous top author dominated the changed lines.
|
|
186
|
+
if (isOverwrite) {
|
|
187
|
+
const validBc = safePercentage(blameCoveragePct);
|
|
188
|
+
const validTopPct = safePercentage(prevTopAuthorPct);
|
|
189
|
+
|
|
190
|
+
if (validBc !== null && validBc > 0 && validTopPct !== null && validTopPct > 0) {
|
|
191
|
+
// Both values are already 0-100, calculate strength
|
|
192
|
+
const strength = (validTopPct / 100) * (validBc / 100);
|
|
193
|
+
c += 0.15 * strength;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// clamp 0..1
|
|
198
|
+
if (c < 0) c = 0;
|
|
199
|
+
if (c > 1) c = 1;
|
|
200
|
+
return c;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Infer overwrite vs previous author based on blame_stats for the change ranges.
|
|
205
|
+
* This matches the spirit of your current node model:
|
|
206
|
+
* - if we have meaningful blame coverage and a prev top author that differs from the commit author => overwrite.
|
|
207
|
+
*/
|
|
208
|
+
function computeOverwrite(cf, commitAuthorId) {
|
|
209
|
+
const blame = cf && cf.blame_stats ? cf.blame_stats : null;
|
|
210
|
+
const coverage = safePercentage(blame && blame.coverage_pct);
|
|
211
|
+
const top =
|
|
212
|
+
blame && blame.prev_authors && Array.isArray(blame.prev_authors.top)
|
|
213
|
+
? blame.prev_authors.top[0]
|
|
214
|
+
: null;
|
|
215
|
+
|
|
216
|
+
const prevTopAuthorId =
|
|
217
|
+
top && typeof top.author_id === "string" ? top.author_id : null;
|
|
218
|
+
const prevTopAuthorPct = safePercentage(top && top.pct);
|
|
219
|
+
|
|
220
|
+
// Can only judge overwrite if:
|
|
221
|
+
// 1. We have blame coverage (file existed before)
|
|
222
|
+
// 2. We have a previous top author
|
|
223
|
+
// 3. We have a commit author to compare against
|
|
224
|
+
const canJudge =
|
|
225
|
+
coverage !== null && coverage > 0 && prevTopAuthorId && commitAuthorId;
|
|
226
|
+
const isOverwrite = !!(canJudge && prevTopAuthorId !== commitAuthorId);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
isOverwrite,
|
|
230
|
+
prevTopAuthorId,
|
|
231
|
+
prevTopAuthorPct,
|
|
232
|
+
blameCoveragePct: coverage,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Returns rows for insertion/upsert into public.file_changes
|
|
238
|
+
*/
|
|
239
|
+
export function buildFileChanges({ commit, changedFiles, snapshotId = null }) {
|
|
240
|
+
console.log("🧩 buildFileChanges:start");
|
|
241
|
+
|
|
242
|
+
if (!commit || !commit.timestamp) {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
if (!Array.isArray(changedFiles) || changedFiles.length === 0) {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const commitTsIso = toIsoDate(commit.timestamp);
|
|
250
|
+
if (!commitTsIso) {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const commitSha = (commit && commit.sha) || null;
|
|
255
|
+
const commitAuthorId =
|
|
256
|
+
(commit && typeof commit.author_id === "string" && commit.author_id) ||
|
|
257
|
+
(changedFiles[0] && changedFiles[0].commit_author_id) ||
|
|
258
|
+
null;
|
|
259
|
+
|
|
260
|
+
const rows = changedFiles
|
|
261
|
+
.map((cf, i) => {
|
|
262
|
+
const path = pickPath(cf);
|
|
263
|
+
if (!path) return null;
|
|
264
|
+
|
|
265
|
+
const status = typeof cf.status === "string" ? cf.status : null;
|
|
266
|
+
|
|
267
|
+
const { insertions, deletions, diffStatsOk, changeSize } = pickDiffStats(cf);
|
|
268
|
+
|
|
269
|
+
// For new files (status "A"), there's no previous version to blame
|
|
270
|
+
// For modified files, we may have blame_stats.lines_changed
|
|
271
|
+
const blameLines = safeNumber(cf?.blame_stats?.lines_changed);
|
|
272
|
+
|
|
273
|
+
// lines_changed: represents the actual lines that changed in the file
|
|
274
|
+
// - For new files: prefer diff stats (total lines added)
|
|
275
|
+
// - For modified files: prefer blame stats (actual changed lines) when available
|
|
276
|
+
// - Fallback to diff stats sum, then cf.lines_changed
|
|
277
|
+
const linesChanged =
|
|
278
|
+
status === "A"
|
|
279
|
+
? changeSize ?? safeNumber(cf?.lines_changed) ?? null
|
|
280
|
+
: blameLines ?? changeSize ?? safeNumber(cf?.lines_changed) ?? null;
|
|
281
|
+
|
|
282
|
+
const locFile =
|
|
283
|
+
safeNumber(cf?.indicators?.loc) ?? safeNumber(cf?.loc) ?? null;
|
|
284
|
+
|
|
285
|
+
// For new files, there is no previous author (isOverwrite will be false)
|
|
286
|
+
const {
|
|
287
|
+
isOverwrite,
|
|
288
|
+
prevTopAuthorId,
|
|
289
|
+
prevTopAuthorPct,
|
|
290
|
+
blameCoveragePct,
|
|
291
|
+
} = status === "A"
|
|
292
|
+
? { isOverwrite: false, prevTopAuthorId: null, prevTopAuthorPct: null, blameCoveragePct: null }
|
|
293
|
+
: computeOverwrite(cf, commitAuthorId);
|
|
294
|
+
|
|
295
|
+
const fileClass =
|
|
296
|
+
(cf && typeof cf.class === "string" && cf.class) ||
|
|
297
|
+
(cf && typeof cf.file_class === "string" && cf.file_class) ||
|
|
298
|
+
inferFileClass(path);
|
|
299
|
+
|
|
300
|
+
// Extract dependency information from enriched changed_files
|
|
301
|
+
const imports = Array.isArray(cf.imports) ? cf.imports : [];
|
|
302
|
+
const importedBy = Array.isArray(cf.imported_by) ? cf.imported_by : [];
|
|
303
|
+
const importCount = imports.length;
|
|
304
|
+
const importedByCount = importedBy.length;
|
|
305
|
+
|
|
306
|
+
// Minimal event typing at "single commit" granularity
|
|
307
|
+
const eventType = isOverwrite ? "overwrite" : "normal";
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
// Core identification
|
|
311
|
+
path,
|
|
312
|
+
file_class: fileClass,
|
|
313
|
+
idx: i, // Per-commit index (0-based, stable within this commit)
|
|
314
|
+
|
|
315
|
+
// Commit context
|
|
316
|
+
snapshot_id: snapshotId,
|
|
317
|
+
commit_sha: commitSha || (cf && cf.commit_sha) || null,
|
|
318
|
+
commit_timestamp: commitTsIso,
|
|
319
|
+
author_id: (cf && cf.commit_author_id) || commitAuthorId || null, // Current commit author (touching)
|
|
320
|
+
|
|
321
|
+
// Change metadata
|
|
322
|
+
status, // Git status: A (added), M (modified), D (deleted), R (renamed)
|
|
323
|
+
lines_changed: linesChanged, // Semantic: actual changed lines (blame or diff)
|
|
324
|
+
insertions: insertions, // Raw git: lines inserted
|
|
325
|
+
deletions: deletions, // Raw git: lines deleted
|
|
326
|
+
change_size: changeSize ?? linesChanged, // Total magnitude (prefer ins+del, fallback to lines_changed)
|
|
327
|
+
diff_stats_ok: !!diffStatsOk, // True if diff stats are valid (not binary)
|
|
328
|
+
|
|
329
|
+
// File metrics
|
|
330
|
+
loc_file: locFile, // Lines of code in the file
|
|
331
|
+
|
|
332
|
+
// Attribution & blame
|
|
333
|
+
blame_coverage_pct: blameCoveragePct, // Percentage of changed lines with blame data (0-100)
|
|
334
|
+
blame_ok: blameCoveragePct !== null && blameCoveragePct > 0,
|
|
335
|
+
prev_top_author_id: prevTopAuthorId, // Previous top author (whose code is being touched)
|
|
336
|
+
prev_top_author_pct: prevTopAuthorPct, // % of changed lines owned by prev author (0-100)
|
|
337
|
+
|
|
338
|
+
// Derived signals
|
|
339
|
+
is_overwrite: isOverwrite, // True when prev_top_author_id differs from author_id
|
|
340
|
+
confidence: computeConfidence({
|
|
341
|
+
blameCoveragePct,
|
|
342
|
+
diffStatsOk: !!diffStatsOk,
|
|
343
|
+
linesChanged,
|
|
344
|
+
status,
|
|
345
|
+
isOverwrite,
|
|
346
|
+
prevTopAuthorPct,
|
|
347
|
+
}), // Confidence score 0.0-1.0
|
|
348
|
+
|
|
349
|
+
// Dependency tracking
|
|
350
|
+
imports: imports, // Array of file paths this file imports
|
|
351
|
+
imported_by: importedBy, // Array of file paths that import this file
|
|
352
|
+
import_count: importCount, // Number of imports (for quick queries)
|
|
353
|
+
imported_by_count: importedByCount, // Fan-in (how many files depend on this)
|
|
354
|
+
|
|
355
|
+
// Cross-commit sequencing (computed server-side via window functions)
|
|
356
|
+
prev_idx: null, // Index of previous change to this file
|
|
357
|
+
delta_lines_changed: null, // Change in lines_changed vs previous
|
|
358
|
+
delta_time_seconds: null, // Time delta vs previous change
|
|
359
|
+
author_changed: null, // True if author_id differs from previous
|
|
360
|
+
|
|
361
|
+
// Event classification
|
|
362
|
+
event_type: eventType, // "overwrite" | "normal" (can extend: "refactor", "cleanup", etc.)
|
|
363
|
+
};
|
|
364
|
+
})
|
|
365
|
+
.filter(Boolean);
|
|
366
|
+
|
|
367
|
+
console.log("🧩 buildFileChanges:end");
|
|
368
|
+
|
|
369
|
+
return rows;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export default buildFileChanges;
|