@eduardbar/drift 1.3.0 → 1.4.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/.gga +50 -0
- package/.github/actions/drift-review/README.md +60 -0
- package/.github/actions/drift-review/action.yml +131 -0
- package/.github/actions/drift-scan/README.md +28 -32
- package/.github/actions/drift-scan/action.yml +78 -14
- package/.github/workflows/review-pr.yml +34 -41
- package/AGENTS.md +75 -251
- package/CHANGELOG.md +28 -0
- package/README.md +148 -41
- package/dist/benchmark.d.ts +1 -1
- package/dist/benchmark.js +71 -52
- package/dist/cli.js +243 -8
- package/dist/config.js +16 -2
- package/dist/diff.js +42 -50
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +133 -0
- package/dist/format.d.ts +17 -0
- package/dist/format.js +45 -0
- package/dist/guard-types.d.ts +57 -0
- package/dist/guard-types.js +2 -0
- package/dist/guard.d.ts +14 -0
- package/dist/guard.js +239 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.js +4 -1
- package/dist/init.d.ts +15 -0
- package/dist/init.js +273 -0
- package/dist/map-cycles.d.ts +2 -0
- package/dist/map-cycles.js +34 -0
- package/dist/map-svg.d.ts +19 -0
- package/dist/map-svg.js +97 -0
- package/dist/map.js +78 -138
- package/dist/metrics.js +70 -55
- package/dist/output-metadata.d.ts +13 -0
- package/dist/output-metadata.js +17 -0
- package/dist/plugins-capabilities.d.ts +4 -0
- package/dist/plugins-capabilities.js +21 -0
- package/dist/plugins-messages.d.ts +10 -0
- package/dist/plugins-messages.js +16 -0
- package/dist/plugins-rules.d.ts +9 -0
- package/dist/plugins-rules.js +137 -0
- package/dist/plugins.d.ts +1 -1
- package/dist/plugins.js +45 -142
- package/dist/reporter-constants.d.ts +16 -0
- package/dist/reporter-constants.js +39 -0
- package/dist/reporter.d.ts +3 -3
- package/dist/reporter.js +35 -55
- package/dist/review.d.ts +2 -1
- package/dist/review.js +2 -1
- package/dist/rules/phase3-configurable.js +23 -15
- package/dist/saas/constants.d.ts +15 -0
- package/dist/saas/constants.js +48 -0
- package/dist/saas/dashboard.d.ts +8 -0
- package/dist/saas/dashboard.js +132 -0
- package/dist/saas/errors.d.ts +19 -0
- package/dist/saas/errors.js +37 -0
- package/dist/saas/helpers.d.ts +21 -0
- package/dist/saas/helpers.js +110 -0
- package/dist/saas/ingest.d.ts +3 -0
- package/dist/saas/ingest.js +249 -0
- package/dist/saas/organization.d.ts +5 -0
- package/dist/saas/organization.js +82 -0
- package/dist/saas/plan-change.d.ts +10 -0
- package/dist/saas/plan-change.js +15 -0
- package/dist/saas/store.d.ts +21 -0
- package/dist/saas/store.js +159 -0
- package/dist/saas/types.d.ts +191 -0
- package/dist/saas/types.js +2 -0
- package/dist/saas.d.ts +8 -218
- package/dist/saas.js +7 -761
- package/dist/sarif.d.ts +74 -0
- package/dist/sarif.js +122 -0
- package/dist/trust-advanced.d.ts +14 -0
- package/dist/trust-advanced.js +65 -0
- package/dist/trust-kpi-fs.d.ts +3 -0
- package/dist/trust-kpi-fs.js +141 -0
- package/dist/trust-kpi-parse.d.ts +7 -0
- package/dist/trust-kpi-parse.js +186 -0
- package/dist/trust-kpi-types.d.ts +16 -0
- package/dist/trust-kpi-types.js +2 -0
- package/dist/trust-kpi.d.ts +1 -3
- package/dist/trust-kpi.js +6 -266
- package/dist/trust-policy.d.ts +32 -0
- package/dist/trust-policy.js +160 -0
- package/dist/trust-render.d.ts +9 -0
- package/dist/trust-render.js +54 -0
- package/dist/trust-scoring.d.ts +9 -0
- package/dist/trust-scoring.js +208 -0
- package/dist/trust.d.ts +4 -32
- package/dist/trust.js +29 -432
- package/dist/types/app.d.ts +30 -0
- package/dist/types/app.js +2 -0
- package/dist/types/config.d.ts +25 -0
- package/dist/types/config.js +2 -0
- package/dist/types/core.d.ts +100 -0
- package/dist/types/core.js +2 -0
- package/dist/types/diff.d.ts +55 -0
- package/dist/types/diff.js +2 -0
- package/dist/types/plugin.d.ts +41 -0
- package/dist/types/plugin.js +2 -0
- package/dist/types/trust.d.ts +120 -0
- package/dist/types/trust.js +2 -0
- package/dist/types.d.ts +8 -365
- package/docs/release-notes-draft.md +40 -0
- package/docs/rules-catalog.md +49 -0
- package/docs/trust-core-release-checklist.md +37 -5
- package/package.json +3 -2
- package/packages/vscode-drift/src/code-actions.ts +1 -1
- package/schemas/drift-ai-output.v1.json +162 -0
- package/schemas/drift-report.v1.json +151 -0
- package/schemas/drift-trust.v1.json +131 -0
- package/scripts/smoke-repo.mjs +394 -0
- package/src/benchmark.ts +75 -53
- package/src/cli.ts +285 -13
- package/src/config.ts +19 -2
- package/src/diff.ts +57 -48
- package/src/doctor.ts +173 -0
- package/src/format.ts +81 -0
- package/src/guard-types.ts +64 -0
- package/src/guard.ts +324 -0
- package/src/index.ts +35 -0
- package/src/init.ts +298 -0
- package/src/map-cycles.ts +38 -0
- package/src/map-svg.ts +124 -0
- package/src/map.ts +111 -142
- package/src/metrics.ts +78 -59
- package/src/output-metadata.ts +30 -0
- package/src/plugins-capabilities.ts +36 -0
- package/src/plugins-messages.ts +35 -0
- package/src/plugins-rules.ts +296 -0
- package/src/plugins.ts +76 -283
- package/src/reporter-constants.ts +46 -0
- package/src/reporter.ts +64 -65
- package/src/review.ts +4 -2
- package/src/rules/phase3-configurable.ts +39 -26
- package/src/saas/constants.ts +56 -0
- package/src/saas/dashboard.ts +172 -0
- package/src/saas/errors.ts +45 -0
- package/src/saas/helpers.ts +140 -0
- package/src/saas/ingest.ts +278 -0
- package/src/saas/organization.ts +99 -0
- package/src/saas/plan-change.ts +19 -0
- package/src/saas/store.ts +172 -0
- package/src/saas/types.ts +216 -0
- package/src/saas.ts +49 -1031
- package/src/sarif.ts +232 -0
- package/src/trust-advanced.ts +99 -0
- package/src/trust-kpi-fs.ts +169 -0
- package/src/trust-kpi-parse.ts +219 -0
- package/src/trust-kpi-types.ts +19 -0
- package/src/trust-kpi.ts +8 -316
- package/src/trust-policy.ts +246 -0
- package/src/trust-render.ts +61 -0
- package/src/trust-scoring.ts +231 -0
- package/src/trust.ts +62 -576
- package/src/types/app.ts +30 -0
- package/src/types/config.ts +27 -0
- package/src/types/core.ts +105 -0
- package/src/types/diff.ts +61 -0
- package/src/types/plugin.ts +46 -0
- package/src/types/trust.ts +134 -0
- package/src/types.ts +78 -409
- package/tests/cli-sarif.test.ts +92 -0
- package/tests/format.test.ts +157 -0
- package/tests/new-features.test.ts +10 -2
- package/tests/phase1-init-doctor-guard.test.ts +199 -0
- package/tests/sarif.test.ts +160 -0
- package/tests/trust-kpi.test.ts +31 -4
- package/tests/trust.test.ts +18 -0
package/dist/trust.js
CHANGED
|
@@ -1,406 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
]);
|
|
9
|
-
const RULE_SUGGESTIONS = {
|
|
10
|
-
'circular-dependency': 'Break cycles first to reduce hidden merge blast radius.',
|
|
11
|
-
'layer-violation': 'Fix layer violations to keep architecture boundaries enforceable.',
|
|
12
|
-
'high-complexity': 'Split branch-heavy functions before adding more logic.',
|
|
13
|
-
'deep-nesting': 'Flatten control flow with early returns.',
|
|
14
|
-
'large-file': 'Split monolithic files by responsibility before merge.',
|
|
15
|
-
'large-function': 'Extract smaller functions to reduce review complexity.',
|
|
16
|
-
'catch-swallow': 'Handle or rethrow swallowed errors to avoid silent failures.',
|
|
17
|
-
'debug-leftover': 'Remove debug leftovers from production paths.',
|
|
18
|
-
'semantic-duplication': 'Consolidate duplicated logic to prevent divergent fixes.',
|
|
19
|
-
'dead-file': 'Delete or wire dead files to avoid stale merge artifacts.',
|
|
20
|
-
};
|
|
21
|
-
const SYSTEMIC_RULES = new Set([
|
|
22
|
-
'circular-dependency',
|
|
23
|
-
'layer-violation',
|
|
24
|
-
'cross-boundary-import',
|
|
25
|
-
'unused-export',
|
|
26
|
-
'unused-dependency',
|
|
27
|
-
'dead-file',
|
|
28
|
-
'semantic-duplication',
|
|
29
|
-
'controller-no-db',
|
|
30
|
-
'service-no-http',
|
|
31
|
-
]);
|
|
32
|
-
function formatTrustGatePolicyValues(values) {
|
|
33
|
-
const enabled = typeof values.enabled === 'boolean' ? String(values.enabled) : 'inherit';
|
|
34
|
-
const minTrust = typeof values.minTrust === 'number' ? String(values.minTrust) : 'inherit';
|
|
35
|
-
const maxRisk = values.maxRisk ?? 'inherit';
|
|
36
|
-
return `enabled=${enabled} minTrust=${minTrust} maxRisk=${maxRisk}`;
|
|
37
|
-
}
|
|
38
|
-
export const MERGE_RISK_ORDER = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'];
|
|
39
|
-
const BRANCH_ENV_CANDIDATES = [
|
|
40
|
-
'DRIFT_BRANCH',
|
|
41
|
-
'GITHUB_HEAD_REF',
|
|
42
|
-
'GITHUB_REF_NAME',
|
|
43
|
-
'CI_COMMIT_REF_NAME',
|
|
44
|
-
'BRANCH_NAME',
|
|
45
|
-
];
|
|
46
|
-
export function normalizeMergeRiskLevel(value) {
|
|
47
|
-
const normalized = value.toUpperCase();
|
|
48
|
-
return MERGE_RISK_ORDER.find((level) => level === normalized);
|
|
49
|
-
}
|
|
50
|
-
function branchPatternToRegExp(pattern) {
|
|
51
|
-
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, '\\$&').replace(/\*/g, '.*');
|
|
52
|
-
return new RegExp(`^${escaped}$`);
|
|
53
|
-
}
|
|
54
|
-
function patternSpecificity(pattern) {
|
|
55
|
-
const wildcardCount = (pattern.match(/\*/g) ?? []).length;
|
|
56
|
-
const staticChars = pattern.replace(/\*/g, '').length;
|
|
57
|
-
const exactBoost = wildcardCount === 0 ? 10_000 : 0;
|
|
58
|
-
return exactBoost + staticChars * 10 - wildcardCount;
|
|
59
|
-
}
|
|
60
|
-
function resolvePresetsForBranch(branchName, presets) {
|
|
61
|
-
if (!presets || presets.length === 0)
|
|
62
|
-
return [];
|
|
63
|
-
const matched = [];
|
|
64
|
-
for (let index = 0; index < presets.length; index += 1) {
|
|
65
|
-
const preset = presets[index];
|
|
66
|
-
if (!preset?.branch)
|
|
67
|
-
continue;
|
|
68
|
-
const regex = branchPatternToRegExp(preset.branch);
|
|
69
|
-
if (!regex.test(branchName))
|
|
70
|
-
continue;
|
|
71
|
-
matched.push({ preset, specificity: patternSpecificity(preset.branch), index });
|
|
72
|
-
}
|
|
73
|
-
matched.sort((a, b) => a.specificity - b.specificity || a.index - b.index);
|
|
74
|
-
return matched.map((entry) => entry.preset);
|
|
75
|
-
}
|
|
76
|
-
function normalizeMinTrust(value) {
|
|
77
|
-
return typeof value === 'number' && !Number.isNaN(value) ? value : undefined;
|
|
78
|
-
}
|
|
79
|
-
function normalizeMaxRisk(value) {
|
|
80
|
-
if (typeof value !== 'string')
|
|
81
|
-
return undefined;
|
|
82
|
-
return normalizeMergeRiskLevel(value);
|
|
83
|
-
}
|
|
84
|
-
function normalizeTrustGateOptions(source) {
|
|
85
|
-
if (!source)
|
|
86
|
-
return {};
|
|
87
|
-
return {
|
|
88
|
-
enabled: typeof source.enabled === 'boolean' ? source.enabled : undefined,
|
|
89
|
-
minTrust: normalizeMinTrust(source.minTrust),
|
|
90
|
-
maxRisk: normalizeMaxRisk(source.maxRisk),
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
function mergeTrustGateOptions(base, layer) {
|
|
94
|
-
return {
|
|
95
|
-
enabled: typeof layer.enabled === 'boolean' ? layer.enabled : base.enabled,
|
|
96
|
-
minTrust: layer.minTrust ?? base.minTrust,
|
|
97
|
-
maxRisk: layer.maxRisk ?? base.maxRisk,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
function normalizeResolutionOptions(branchNameOrOptions, explicitOverrides) {
|
|
101
|
-
if (typeof branchNameOrOptions === 'string') {
|
|
102
|
-
return {
|
|
103
|
-
branchName: branchNameOrOptions,
|
|
104
|
-
overrides: explicitOverrides,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
if (!branchNameOrOptions) {
|
|
108
|
-
return {
|
|
109
|
-
overrides: explicitOverrides,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
return {
|
|
113
|
-
...branchNameOrOptions,
|
|
114
|
-
overrides: explicitOverrides
|
|
115
|
-
? mergeTrustGateOptions(normalizeTrustGateOptions(branchNameOrOptions.overrides), normalizeTrustGateOptions(explicitOverrides))
|
|
116
|
-
: branchNameOrOptions.overrides,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
function resolvePolicyPack(policyPacks, policyPackName) {
|
|
120
|
-
const normalizedName = policyPackName?.trim();
|
|
121
|
-
if (!normalizedName)
|
|
122
|
-
return {};
|
|
123
|
-
if (!policyPacks) {
|
|
124
|
-
return { name: normalizedName, invalid: normalizedName };
|
|
125
|
-
}
|
|
126
|
-
const pack = policyPacks[normalizedName];
|
|
127
|
-
if (!pack) {
|
|
128
|
-
return { name: normalizedName, invalid: normalizedName };
|
|
129
|
-
}
|
|
130
|
-
return { name: normalizedName, pack };
|
|
131
|
-
}
|
|
132
|
-
export function detectBranchName(env = process.env) {
|
|
133
|
-
for (const key of BRANCH_ENV_CANDIDATES) {
|
|
134
|
-
const value = env[key]?.trim();
|
|
135
|
-
if (value)
|
|
136
|
-
return value;
|
|
137
|
-
}
|
|
138
|
-
return undefined;
|
|
139
|
-
}
|
|
140
|
-
export function explainTrustGatePolicy(config, branchNameOrOptions, explicitOverrides) {
|
|
141
|
-
const policy = config?.trustGate;
|
|
142
|
-
const resolution = normalizeResolutionOptions(branchNameOrOptions, explicitOverrides);
|
|
143
|
-
const normalizedBranch = resolution.branchName?.trim();
|
|
144
|
-
const packResolution = resolvePolicyPack(policy?.policyPacks, resolution.policyPack);
|
|
145
|
-
const steps = [];
|
|
146
|
-
const base = normalizeTrustGateOptions(policy);
|
|
147
|
-
let effective = base;
|
|
148
|
-
steps.push({ source: 'base', name: 'trustGate', values: base });
|
|
149
|
-
if (packResolution.pack) {
|
|
150
|
-
const packOptions = normalizeTrustGateOptions(packResolution.pack);
|
|
151
|
-
effective = mergeTrustGateOptions(effective, packOptions);
|
|
152
|
-
steps.push({ source: 'policy-pack', name: packResolution.name ?? 'unknown', values: packOptions });
|
|
153
|
-
}
|
|
154
|
-
if (normalizedBranch) {
|
|
155
|
-
const matchedPresets = resolvePresetsForBranch(normalizedBranch, policy?.presets);
|
|
156
|
-
for (const preset of matchedPresets) {
|
|
157
|
-
const presetOptions = normalizeTrustGateOptions(preset);
|
|
158
|
-
effective = mergeTrustGateOptions(effective, presetOptions);
|
|
159
|
-
steps.push({ source: 'branch-preset', name: preset.branch, values: presetOptions });
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
const overrides = normalizeTrustGateOptions(resolution.overrides);
|
|
163
|
-
if (Object.values(overrides).some((value) => value !== undefined)) {
|
|
164
|
-
effective = mergeTrustGateOptions(effective, overrides);
|
|
165
|
-
steps.push({ source: 'overrides', name: 'cli', values: overrides });
|
|
166
|
-
}
|
|
167
|
-
return {
|
|
168
|
-
effectivePolicy: effective,
|
|
169
|
-
branchName: normalizedBranch,
|
|
170
|
-
selectedPolicyPack: packResolution.name,
|
|
171
|
-
invalidPolicyPack: packResolution.invalid,
|
|
172
|
-
steps,
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
export function resolveTrustGatePolicy(config, branchNameOrOptions, explicitOverrides) {
|
|
176
|
-
const options = normalizeResolutionOptions(branchNameOrOptions, explicitOverrides);
|
|
177
|
-
return explainTrustGatePolicy(config, options).effectivePolicy;
|
|
178
|
-
}
|
|
179
|
-
export function formatTrustGatePolicyExplanation(explanation) {
|
|
180
|
-
const lines = ['Trust gate policy resolution:'];
|
|
181
|
-
lines.push(`- branch: ${explanation.branchName ?? 'not provided'}`);
|
|
182
|
-
lines.push(`- policy pack: ${explanation.selectedPolicyPack ?? 'not selected'}`);
|
|
183
|
-
if (explanation.invalidPolicyPack) {
|
|
184
|
-
lines.push(`- invalid policy pack: ${explanation.invalidPolicyPack}`);
|
|
185
|
-
}
|
|
186
|
-
lines.push('- steps:');
|
|
187
|
-
for (const [index, step] of explanation.steps.entries()) {
|
|
188
|
-
lines.push(` ${index + 1}. ${step.source} (${step.name}): ${formatTrustGatePolicyValues(step.values)}`);
|
|
189
|
-
}
|
|
190
|
-
lines.push(`- effective: ${formatTrustGatePolicyValues(explanation.effectivePolicy)}`);
|
|
191
|
-
return lines.join('\n');
|
|
192
|
-
}
|
|
193
|
-
function clamp(value, min, max) {
|
|
194
|
-
return Math.max(min, Math.min(max, value));
|
|
195
|
-
}
|
|
196
|
-
function toMergeRisk(trustScore) {
|
|
197
|
-
if (trustScore >= 80)
|
|
198
|
-
return 'LOW';
|
|
199
|
-
if (trustScore >= 60)
|
|
200
|
-
return 'MEDIUM';
|
|
201
|
-
if (trustScore >= 40)
|
|
202
|
-
return 'HIGH';
|
|
203
|
-
return 'CRITICAL';
|
|
204
|
-
}
|
|
205
|
-
function computeReasons(report) {
|
|
206
|
-
const architectureIssues = Object.entries(report.summary.byRule)
|
|
207
|
-
.filter(([rule]) => ARCHITECTURE_RULES.has(rule))
|
|
208
|
-
.reduce((sum, [, count]) => sum + count, 0);
|
|
209
|
-
const worstHotspot = report.maintenanceRisk.hotspots[0];
|
|
210
|
-
const reasons = [
|
|
211
|
-
{
|
|
212
|
-
label: 'Drift score pressure',
|
|
213
|
-
detail: `Repository drift score is ${report.totalScore}/100.`,
|
|
214
|
-
impact: Math.round(report.totalScore * 0.55),
|
|
215
|
-
},
|
|
216
|
-
{
|
|
217
|
-
label: 'Error-level issues',
|
|
218
|
-
detail: `${report.summary.errors} error issue(s) increase merge volatility.`,
|
|
219
|
-
impact: Math.min(22, report.summary.errors * 4),
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
label: 'Architecture signals',
|
|
223
|
-
detail: `${architectureIssues} architecture-related issue(s) detected.`,
|
|
224
|
-
impact: Math.min(24, architectureIssues * 6),
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
label: 'Maintenance hotspots',
|
|
228
|
-
detail: `Maintenance risk is ${report.maintenanceRisk.level.toUpperCase()} (${report.maintenanceRisk.score}/100).`,
|
|
229
|
-
impact: Math.min(25, Math.round(report.maintenanceRisk.score * 0.25)),
|
|
230
|
-
},
|
|
231
|
-
{
|
|
232
|
-
label: 'Highest-risk file',
|
|
233
|
-
detail: worstHotspot
|
|
234
|
-
? `${worstHotspot.file} has hotspot risk ${worstHotspot.risk}/100.`
|
|
235
|
-
: 'No hotspot concentration detected.',
|
|
236
|
-
impact: worstHotspot ? Math.min(15, Math.round(worstHotspot.risk * 0.15)) : 0,
|
|
237
|
-
},
|
|
238
|
-
];
|
|
239
|
-
return reasons
|
|
240
|
-
.filter((reason) => reason.impact > 0)
|
|
241
|
-
.sort((a, b) => b.impact - a.impact)
|
|
242
|
-
.slice(0, 4);
|
|
243
|
-
}
|
|
244
|
-
function effortFromWeight(weight) {
|
|
245
|
-
if (weight <= 6)
|
|
246
|
-
return 'low';
|
|
247
|
-
if (weight <= 12)
|
|
248
|
-
return 'medium';
|
|
249
|
-
return 'high';
|
|
250
|
-
}
|
|
251
|
-
function computeDiffContext(diff) {
|
|
252
|
-
const scoreRegressionPenalty = Math.max(0, diff.totalDelta) * 2;
|
|
253
|
-
const newIssuePenalty = diff.newIssuesCount * 3;
|
|
254
|
-
const churnPenalty = diff.files.length >= 15 ? 4 : 0;
|
|
255
|
-
const penalty = clamp(scoreRegressionPenalty + newIssuePenalty + churnPenalty, 0, 30);
|
|
256
|
-
const scoreImprovementBonus = Math.max(0, -diff.totalDelta) * 2;
|
|
257
|
-
const resolvedIssueBonus = diff.resolvedIssuesCount * 2;
|
|
258
|
-
const bonus = clamp(scoreImprovementBonus + resolvedIssueBonus, 0, 20);
|
|
259
|
-
const netImpact = penalty - bonus;
|
|
260
|
-
const status = netImpact > 0 ? 'regressed' : netImpact < 0 ? 'improved' : 'neutral';
|
|
261
|
-
return {
|
|
262
|
-
baseRef: diff.baseRef,
|
|
263
|
-
status,
|
|
264
|
-
scoreDelta: diff.totalDelta,
|
|
265
|
-
newIssues: diff.newIssuesCount,
|
|
266
|
-
resolvedIssues: diff.resolvedIssuesCount,
|
|
267
|
-
filesChanged: diff.files.length,
|
|
268
|
-
penalty,
|
|
269
|
-
bonus,
|
|
270
|
-
netImpact,
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
function confidenceFromPrioritySignals(occurrences, severity, systemic) {
|
|
274
|
-
const severityScore = severity === 'error' ? 4 : severity === 'warning' ? 2 : 1;
|
|
275
|
-
const systemicScore = systemic ? 2 : 0;
|
|
276
|
-
const score = occurrences * 2 + severityScore + systemicScore;
|
|
277
|
-
if (score >= 12)
|
|
278
|
-
return 'high';
|
|
279
|
-
if (score >= 7)
|
|
280
|
-
return 'medium';
|
|
281
|
-
return 'low';
|
|
282
|
-
}
|
|
283
|
-
function computeFixPriorities(report, advancedMode = false) {
|
|
284
|
-
const ordered = Object.entries(report.summary.byRule)
|
|
285
|
-
.map(([rule, occurrences]) => {
|
|
286
|
-
const weightConfig = RULE_WEIGHTS[rule] ?? { severity: 'warning', weight: 6 };
|
|
287
|
-
const severityBoost = weightConfig.severity === 'error' ? 25 : weightConfig.severity === 'warning' ? 12 : 4;
|
|
288
|
-
const systemic = SYSTEMIC_RULES.has(rule);
|
|
289
|
-
const systemicBoost = advancedMode && systemic ? 25 : 0;
|
|
290
|
-
const priorityScore = occurrences * weightConfig.weight + severityBoost + systemicBoost;
|
|
291
|
-
const confidence = confidenceFromPrioritySignals(occurrences, weightConfig.severity, systemic);
|
|
292
|
-
const explanation = advancedMode
|
|
293
|
-
? systemic
|
|
294
|
-
? 'System-level rule that propagates risk across multiple teams and modules.'
|
|
295
|
-
: 'Local rule with contained impact; treat as team-level cleanup after systemic fixes.'
|
|
296
|
-
: undefined;
|
|
297
|
-
return {
|
|
298
|
-
rule,
|
|
299
|
-
severity: weightConfig.severity,
|
|
300
|
-
occurrences,
|
|
301
|
-
systemic,
|
|
302
|
-
priorityScore,
|
|
303
|
-
estimatedTrustGain: Math.min(30, Math.max(3, Math.round(priorityScore / 4))),
|
|
304
|
-
effort: effortFromWeight(weightConfig.weight),
|
|
305
|
-
suggestion: RULE_SUGGESTIONS[rule] ?? 'Address this rule in the highest-scored files first.',
|
|
306
|
-
confidence,
|
|
307
|
-
explanation,
|
|
308
|
-
};
|
|
309
|
-
})
|
|
310
|
-
.sort((a, b) => b.priorityScore - a.priorityScore)
|
|
311
|
-
.slice(0, 5);
|
|
312
|
-
return ordered.map((item, index) => ({
|
|
313
|
-
rank: index + 1,
|
|
314
|
-
rule: item.rule,
|
|
315
|
-
severity: item.severity,
|
|
316
|
-
occurrences: item.occurrences,
|
|
317
|
-
estimated_trust_gain: item.estimatedTrustGain,
|
|
318
|
-
effort: item.effort,
|
|
319
|
-
suggestion: item.suggestion,
|
|
320
|
-
...(advancedMode ? { confidence: item.confidence, explanation: item.explanation, systemic: item.systemic } : {}),
|
|
321
|
-
}));
|
|
322
|
-
}
|
|
323
|
-
function buildComparisonFromPreviousTrust(trustScore, previousTrust) {
|
|
324
|
-
if (!previousTrust || typeof previousTrust.trust_score !== 'number')
|
|
325
|
-
return undefined;
|
|
326
|
-
const trustDelta = trustScore - previousTrust.trust_score;
|
|
327
|
-
const trend = trustDelta > 0 ? 'improving' : trustDelta < 0 ? 'regressing' : 'stable';
|
|
328
|
-
return {
|
|
329
|
-
source: 'previous-trust-json',
|
|
330
|
-
trend,
|
|
331
|
-
summary: `Trust moved ${trustDelta >= 0 ? '+' : ''}${trustDelta} vs provided previous trust JSON.`,
|
|
332
|
-
trust_delta: trustDelta,
|
|
333
|
-
previous_trust_score: previousTrust.trust_score,
|
|
334
|
-
previous_merge_risk: previousTrust.merge_risk,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
function buildComparisonFromSnapshotHistory(report, snapshots) {
|
|
338
|
-
const lastSnapshot = snapshots && snapshots.length > 0 ? snapshots[snapshots.length - 1] : undefined;
|
|
339
|
-
if (!lastSnapshot)
|
|
340
|
-
return undefined;
|
|
341
|
-
const snapshotScoreDelta = report.totalScore - lastSnapshot.score;
|
|
342
|
-
const trend = snapshotScoreDelta < 0 ? 'improving' : snapshotScoreDelta > 0 ? 'regressing' : 'stable';
|
|
343
|
-
const snapshotContext = lastSnapshot.label
|
|
344
|
-
? `${lastSnapshot.timestamp} (${lastSnapshot.label})`
|
|
345
|
-
: lastSnapshot.timestamp;
|
|
346
|
-
return {
|
|
347
|
-
source: 'snapshot-history',
|
|
348
|
-
trend,
|
|
349
|
-
summary: `Drift score moved ${snapshotScoreDelta >= 0 ? '+' : ''}${snapshotScoreDelta} vs snapshot ${snapshotContext}.`,
|
|
350
|
-
snapshot_score_delta: snapshotScoreDelta,
|
|
351
|
-
snapshot_label: lastSnapshot.label || undefined,
|
|
352
|
-
snapshot_timestamp: lastSnapshot.timestamp,
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
function buildTeamGuidance(priorities, comparison, diffContext) {
|
|
356
|
-
const systemicTargets = priorities
|
|
357
|
-
.filter((priority) => priority.systemic)
|
|
358
|
-
.slice(0, 2)
|
|
359
|
-
.map((priority) => `${priority.rule} (x${priority.occurrences})`);
|
|
360
|
-
const guidance = [];
|
|
361
|
-
if (systemicTargets.length > 0) {
|
|
362
|
-
guidance.push(`Start with systemic rules: ${systemicTargets.join(', ')}.`);
|
|
363
|
-
}
|
|
364
|
-
if (comparison?.trend === 'regressing') {
|
|
365
|
-
guidance.push('Trend regressed; freeze net-new debt in CI and assign owners per systemic rule.');
|
|
366
|
-
}
|
|
367
|
-
if (diffContext && diffContext.newIssues > 0) {
|
|
368
|
-
guidance.push(`Block net-new issue growth first (+${diffContext.newIssues} new issue(s) in diff context).`);
|
|
369
|
-
}
|
|
370
|
-
if (guidance.length === 0) {
|
|
371
|
-
guidance.push('Maintain current baseline and schedule periodic systemic debt cleanup by rule ownership.');
|
|
372
|
-
}
|
|
373
|
-
return guidance.slice(0, 3);
|
|
374
|
-
}
|
|
1
|
+
import { MERGE_RISK_ORDER } from './trust-policy.js';
|
|
2
|
+
import { buildAdvancedContext } from './trust-advanced.js';
|
|
3
|
+
import { TOP_REASONS_SLICE, buildDiffRegressionReason, clamp, computeDiffContext, computeFixPriorities, computeReasons, toMergeRisk, } from './trust-scoring.js';
|
|
4
|
+
import { renderTrustAdvancedComparison, renderTrustAdvancedGuidance, renderTrustDiffBlock, renderTrustMarkdownPriorities, renderTrustMarkdownReasons, renderTrustPriorities, renderTrustReasons, } from './trust-render.js';
|
|
5
|
+
import { OUTPUT_SCHEMA, withOutputMetadata } from './output-metadata.js';
|
|
6
|
+
export { MERGE_RISK_ORDER, detectBranchName, explainTrustGatePolicy, formatTrustGatePolicyExplanation, normalizeMergeRiskLevel, resolveTrustGatePolicy, } from './trust-policy.js';
|
|
7
|
+
const CONSOLE_DIFF_INSERT_INDEX = 5;
|
|
375
8
|
export function buildTrustReport(report, options) {
|
|
376
9
|
const reasons = computeReasons(report);
|
|
377
|
-
const advancedMode = options?.advanced?.enabled === true;
|
|
378
10
|
const diffContext = options?.diff ? computeDiffContext(options.diff) : undefined;
|
|
379
11
|
if (diffContext && diffContext.netImpact > 0) {
|
|
380
|
-
reasons.push(
|
|
381
|
-
label: 'Diff regression signals',
|
|
382
|
-
detail: `Against ${diffContext.baseRef}: score delta ${diffContext.scoreDelta >= 0 ? '+' : ''}${diffContext.scoreDelta}, +${diffContext.newIssues} new issue(s), -${diffContext.resolvedIssues} resolved.`,
|
|
383
|
-
impact: diffContext.netImpact,
|
|
384
|
-
});
|
|
12
|
+
reasons.push(buildDiffRegressionReason(diffContext));
|
|
385
13
|
}
|
|
386
14
|
const rankedReasons = reasons
|
|
387
15
|
.filter((reason) => reason.impact > 0)
|
|
388
16
|
.sort((a, b) => b.impact - a.impact)
|
|
389
|
-
.slice(0,
|
|
17
|
+
.slice(0, TOP_REASONS_SLICE);
|
|
390
18
|
const totalPenalty = rankedReasons.reduce((sum, reason) => sum + reason.impact, 0);
|
|
391
19
|
const totalBonus = diffContext && diffContext.netImpact < 0 ? Math.abs(diffContext.netImpact) : 0;
|
|
392
20
|
const trustScore = clamp(Math.round(100 - totalPenalty + totalBonus), 0, 100);
|
|
393
|
-
const
|
|
394
|
-
? buildComparisonFromPreviousTrust(trustScore, options?.advanced?.previousTrust)
|
|
395
|
-
?? buildComparisonFromSnapshotHistory(report, options?.advanced?.snapshots)
|
|
396
|
-
: undefined;
|
|
21
|
+
const advancedMode = options?.advanced?.enabled === true;
|
|
397
22
|
const fixPriorities = computeFixPriorities(report, advancedMode);
|
|
398
|
-
const advancedContext =
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
23
|
+
const advancedContext = buildAdvancedContext({
|
|
24
|
+
report,
|
|
25
|
+
advancedOptions: options?.advanced,
|
|
26
|
+
trustScore,
|
|
27
|
+
fixPriorities,
|
|
28
|
+
diffContext,
|
|
29
|
+
});
|
|
404
30
|
return {
|
|
405
31
|
scannedAt: new Date().toISOString(),
|
|
406
32
|
targetPath: report.targetPath,
|
|
@@ -423,14 +49,8 @@ export function formatTrustConsole(trust) {
|
|
|
423
49
|
`- impact: +${diffContext.penalty} penalty / -${diffContext.bonus} bonus (net ${diffContext.netImpact >= 0 ? '+' : ''}${diffContext.netImpact})`,
|
|
424
50
|
].join('\n')
|
|
425
51
|
: undefined;
|
|
426
|
-
const reasons = trust.top_reasons
|
|
427
|
-
|
|
428
|
-
: trust.top_reasons.map((reason) => `- ${reason.label}: ${reason.detail} (impact ${reason.impact})`).join('\n');
|
|
429
|
-
const priorities = trust.fix_priorities.length === 0
|
|
430
|
-
? '- none'
|
|
431
|
-
: trust.fix_priorities
|
|
432
|
-
.map((priority) => `- #${priority.rank} ${priority.rule} (${priority.severity}, x${priority.occurrences}${priority.confidence ? `, confidence ${priority.confidence}` : ''}): ${priority.suggestion}`)
|
|
433
|
-
.join('\n');
|
|
52
|
+
const reasons = renderTrustReasons(trust.top_reasons);
|
|
53
|
+
const priorities = renderTrustPriorities(trust.fix_priorities);
|
|
434
54
|
const advanced = trust.advanced_context;
|
|
435
55
|
const advancedComparison = advanced?.comparison
|
|
436
56
|
? [
|
|
@@ -455,7 +75,7 @@ export function formatTrustConsole(trust) {
|
|
|
455
75
|
priorities,
|
|
456
76
|
];
|
|
457
77
|
if (diffLines) {
|
|
458
|
-
sections.splice(
|
|
78
|
+
sections.splice(CONSOLE_DIFF_INSERT_INDEX, 0, 'Diff Context:', diffLines, '');
|
|
459
79
|
}
|
|
460
80
|
if (advanced) {
|
|
461
81
|
sections.push('', 'Advanced Team Guidance:', advancedComparison, '', advancedGuidance);
|
|
@@ -463,37 +83,11 @@ export function formatTrustConsole(trust) {
|
|
|
463
83
|
return sections.join('\n');
|
|
464
84
|
}
|
|
465
85
|
export function formatTrustMarkdown(trust) {
|
|
466
|
-
const
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
? '- none'
|
|
472
|
-
: trust.fix_priorities
|
|
473
|
-
.map((priority) => `- #${priority.rank} \`${priority.rule}\` (${priority.severity}, x${priority.occurrences}, effort: ${priority.effort}${priority.confidence ? `, confidence: ${priority.confidence}` : ''}) - ${priority.suggestion}${priority.explanation ? ` ${priority.explanation}` : ''}`)
|
|
474
|
-
.join('\n');
|
|
475
|
-
const diffBlock = !diffContext
|
|
476
|
-
? [
|
|
477
|
-
'- Base ref: not provided',
|
|
478
|
-
'- Diff-aware adjustment: not applied',
|
|
479
|
-
].join('\n')
|
|
480
|
-
: [
|
|
481
|
-
`- Base ref: \`${diffContext.baseRef}\``,
|
|
482
|
-
`- Diff status: **${diffContext.status.toUpperCase()}**`,
|
|
483
|
-
`- Score delta: **${diffContext.scoreDelta >= 0 ? '+' : ''}${diffContext.scoreDelta}**`,
|
|
484
|
-
`- Issues: **+${diffContext.newIssues}** new / **-${diffContext.resolvedIssues}** resolved`,
|
|
485
|
-
`- Trust adjustment: **+${diffContext.penalty}** penalty / **-${diffContext.bonus}** bonus (net ${diffContext.netImpact >= 0 ? '+' : ''}${diffContext.netImpact})`,
|
|
486
|
-
].join('\n');
|
|
487
|
-
const advancedComparison = trust.advanced_context?.comparison
|
|
488
|
-
? [
|
|
489
|
-
`- Source: \`${trust.advanced_context.comparison.source}\``,
|
|
490
|
-
`- Trend: **${trust.advanced_context.comparison.trend.toUpperCase()}**`,
|
|
491
|
-
`- Summary: ${trust.advanced_context.comparison.summary}`,
|
|
492
|
-
].join('\n')
|
|
493
|
-
: '- Historical comparison not available';
|
|
494
|
-
const advancedGuidance = trust.advanced_context?.team_guidance?.length
|
|
495
|
-
? trust.advanced_context.team_guidance.map((item) => `- ${item}`).join('\n')
|
|
496
|
-
: '- none';
|
|
86
|
+
const reasons = renderTrustMarkdownReasons(trust.top_reasons);
|
|
87
|
+
const priorities = renderTrustMarkdownPriorities(trust.fix_priorities);
|
|
88
|
+
const diffBlock = renderTrustDiffBlock(trust.diff_context);
|
|
89
|
+
const advancedComparison = renderTrustAdvancedComparison(trust.advanced_context);
|
|
90
|
+
const advancedGuidance = renderTrustAdvancedGuidance(trust.advanced_context);
|
|
497
91
|
const sections = [
|
|
498
92
|
'## drift trust',
|
|
499
93
|
'',
|
|
@@ -515,8 +109,11 @@ export function formatTrustMarkdown(trust) {
|
|
|
515
109
|
}
|
|
516
110
|
return sections.join('\n');
|
|
517
111
|
}
|
|
112
|
+
function formatTrustJsonObject(trust) {
|
|
113
|
+
return withOutputMetadata(trust, OUTPUT_SCHEMA.trust);
|
|
114
|
+
}
|
|
518
115
|
export function formatTrustJson(trust) {
|
|
519
|
-
return JSON.stringify(trust, null, 2);
|
|
116
|
+
return JSON.stringify(formatTrustJsonObject(trust), null, 2);
|
|
520
117
|
}
|
|
521
118
|
export function renderTrustOutput(trust, options) {
|
|
522
119
|
if (options?.json)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { DriftPerformanceConfig, LayerDefinition, ModuleBoundary } from './config.js';
|
|
2
|
+
import type { TrustGatePolicyConfig } from './trust.js';
|
|
3
|
+
export interface DriftConfig {
|
|
4
|
+
layers?: LayerDefinition[];
|
|
5
|
+
modules?: ModuleBoundary[];
|
|
6
|
+
moduleBoundaries?: ModuleBoundary[];
|
|
7
|
+
boundaries?: ModuleBoundary[];
|
|
8
|
+
plugins?: string[];
|
|
9
|
+
performance?: DriftPerformanceConfig;
|
|
10
|
+
architectureRules?: {
|
|
11
|
+
controllerNoDb?: boolean;
|
|
12
|
+
serviceNoHttp?: boolean;
|
|
13
|
+
maxFunctionLines?: number;
|
|
14
|
+
};
|
|
15
|
+
saas?: {
|
|
16
|
+
freeUserThreshold?: number;
|
|
17
|
+
maxRunsPerWorkspacePerMonth?: number;
|
|
18
|
+
maxReposPerWorkspace?: number;
|
|
19
|
+
retentionDays?: number;
|
|
20
|
+
strictActorEnforcement?: boolean;
|
|
21
|
+
maxWorkspacesPerOrganizationByPlan?: {
|
|
22
|
+
free?: number;
|
|
23
|
+
sponsor?: number;
|
|
24
|
+
team?: number;
|
|
25
|
+
business?: number;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
trustGate?: TrustGatePolicyConfig;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=app.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface LayerDefinition {
|
|
2
|
+
name: string;
|
|
3
|
+
patterns: string[];
|
|
4
|
+
canImportFrom: string[];
|
|
5
|
+
}
|
|
6
|
+
export interface ModuleBoundary {
|
|
7
|
+
name: string;
|
|
8
|
+
root: string;
|
|
9
|
+
allowedExternalImports?: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface DriftPerformanceConfig {
|
|
12
|
+
lowMemory?: boolean;
|
|
13
|
+
chunkSize?: number;
|
|
14
|
+
maxFiles?: number;
|
|
15
|
+
maxFileSizeKb?: number;
|
|
16
|
+
includeSemanticDuplication?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface DriftAnalysisOptions {
|
|
19
|
+
lowMemory?: boolean;
|
|
20
|
+
chunkSize?: number;
|
|
21
|
+
maxFiles?: number;
|
|
22
|
+
maxFileSizeKb?: number;
|
|
23
|
+
includeSemanticDuplication?: boolean;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export interface DriftIssue {
|
|
2
|
+
rule: string;
|
|
3
|
+
severity: 'error' | 'warning' | 'info';
|
|
4
|
+
message: string;
|
|
5
|
+
line: number;
|
|
6
|
+
column: number;
|
|
7
|
+
snippet: string;
|
|
8
|
+
}
|
|
9
|
+
export interface FileReport {
|
|
10
|
+
path: string;
|
|
11
|
+
issues: DriftIssue[];
|
|
12
|
+
score: number;
|
|
13
|
+
}
|
|
14
|
+
export interface RepoQualityScore {
|
|
15
|
+
overall: number;
|
|
16
|
+
dimensions: {
|
|
17
|
+
architecture: number;
|
|
18
|
+
complexity: number;
|
|
19
|
+
'ai-patterns': number;
|
|
20
|
+
testing: number;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export interface RiskHotspot {
|
|
24
|
+
file: string;
|
|
25
|
+
driftScore: number;
|
|
26
|
+
complexityIssues: number;
|
|
27
|
+
hasNearbyTests: boolean;
|
|
28
|
+
changeFrequency: number;
|
|
29
|
+
risk: number;
|
|
30
|
+
reasons: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface MaintenanceRiskMetrics {
|
|
33
|
+
score: number;
|
|
34
|
+
level: 'low' | 'medium' | 'high' | 'critical';
|
|
35
|
+
hotspots: RiskHotspot[];
|
|
36
|
+
signals: {
|
|
37
|
+
highComplexityFiles: number;
|
|
38
|
+
filesWithoutNearbyTests: number;
|
|
39
|
+
frequentChangeFiles: number;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export interface DriftReport {
|
|
43
|
+
scannedAt: string;
|
|
44
|
+
targetPath: string;
|
|
45
|
+
files: FileReport[];
|
|
46
|
+
totalIssues: number;
|
|
47
|
+
totalScore: number;
|
|
48
|
+
totalFiles: number;
|
|
49
|
+
summary: {
|
|
50
|
+
errors: number;
|
|
51
|
+
warnings: number;
|
|
52
|
+
infos: number;
|
|
53
|
+
byRule: Record<string, number>;
|
|
54
|
+
};
|
|
55
|
+
quality: RepoQualityScore;
|
|
56
|
+
maintenanceRisk: MaintenanceRiskMetrics;
|
|
57
|
+
}
|
|
58
|
+
export interface AIIssue {
|
|
59
|
+
rank: number;
|
|
60
|
+
file: string;
|
|
61
|
+
line: number;
|
|
62
|
+
rule: string;
|
|
63
|
+
severity: string;
|
|
64
|
+
message: string;
|
|
65
|
+
snippet: string;
|
|
66
|
+
fix_suggestion: string;
|
|
67
|
+
effort: 'low' | 'medium' | 'high';
|
|
68
|
+
}
|
|
69
|
+
export interface AIOutput {
|
|
70
|
+
summary: {
|
|
71
|
+
score: number;
|
|
72
|
+
grade: string;
|
|
73
|
+
total_issues: number;
|
|
74
|
+
files_affected: number;
|
|
75
|
+
files_clean: number;
|
|
76
|
+
ai_likelihood: number;
|
|
77
|
+
ai_code_smell_score: number;
|
|
78
|
+
};
|
|
79
|
+
files_suspected: Array<{
|
|
80
|
+
path: string;
|
|
81
|
+
ai_likelihood: number;
|
|
82
|
+
triggers: string[];
|
|
83
|
+
}>;
|
|
84
|
+
priority_order: AIIssue[];
|
|
85
|
+
maintenance_risk: MaintenanceRiskMetrics;
|
|
86
|
+
quality: RepoQualityScore;
|
|
87
|
+
context_for_ai: {
|
|
88
|
+
project_type: string;
|
|
89
|
+
scan_path: string;
|
|
90
|
+
rules_detected: string[];
|
|
91
|
+
recommended_action: string;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export interface DriftOutputMetadata {
|
|
95
|
+
$schema: string;
|
|
96
|
+
toolVersion: string;
|
|
97
|
+
}
|
|
98
|
+
export type DriftReportJson = DriftReport & DriftOutputMetadata;
|
|
99
|
+
export type AIOutputJson = AIOutput & DriftOutputMetadata;
|
|
100
|
+
//# sourceMappingURL=core.d.ts.map
|