@@ -65,7 +65,6 @@ function summarize(payload) {
6565 complexity : len ( cat . complexity ) ,
6666 domainTerms : len ( cat . domainTerms ) ,
6767 architecture : len ( cat . architecture ) ,
68- // Optional counts (informational)
6968 counts : {
7069 errors : num ( cat . counts && cat . counts . errors ) ,
7170 warnings : num ( cat . counts && cat . counts . warnings ) ,
@@ -133,7 +132,6 @@ function compare(base, curr) {
133132 const c = num ( curr [ f ] ) ;
134133 if ( c > b ) deltas . push ( { key : f , base : b , current : c , type : 'count' } ) ;
135134 }
136- // Effort ratchet (optional, do not fail but report increases)
137135 const effortInc = [ ] ;
138136 for ( const f of fields ) {
139137 const b = num ( base . effortByCategory && base . effortByCategory [ f ] ) ;
@@ -143,34 +141,80 @@ function compare(base, curr) {
143141 return { deltas, effortInc } ;
144142}
145143
146- function main ( ) {
147- const args = parseArgs ( process . argv ) ;
148- const baselinePath = args . baseline || args . _ [ 0 ] || 'analysis-baseline.json' ;
149- const currentPath = args . current || args . _ [ 1 ] || 'analysis-current.json' ;
144+ function detectIntentFromMetrics ( base , curr ) {
145+ const totalDelta = ( curr . magicNumbers - base . magicNumbers ) +
146+ ( curr . complexity - base . complexity ) +
147+ ( curr . domainTerms - base . domainTerms ) +
148+ ( curr . architecture - base . architecture ) ;
150149
151- const base = readJson ( baselinePath ) ;
152- if ( ! base ) {
153- console . log ( `[ratchet] Baseline not found at ${ baselinePath } .\n` +
154- 'Run: npm run lint:json && npm run analyze:baseline\n' +
155- 'Skipping ratchet (non-blocking)' ) ;
156- process . exit ( 0 ) ;
157- return ;
150+ let intent = 'neutral' ;
151+ let confidence = 0.5 ;
152+ const signals = [ ] ;
153+
154+ if ( totalDelta < - 10 ) {
155+ intent = 'cleanup' ;
156+ confidence = 0.7 ;
157+ signals . push ( 'Violations decreased significantly' ) ;
158+ } else if ( totalDelta > 20 && curr . domainTerms > base . domainTerms * 1.3 ) {
159+ intent = 'ai-generation-suspect' ;
160+ confidence = 0.6 ;
161+ signals . push ( 'Large increase in domain term violations' ) ;
162+ signals . push ( 'Rapid violation growth pattern' ) ;
163+ } else if ( curr . complexity < base . complexity * 0.8 && curr . architecture < base . architecture * 0.8 ) {
164+ intent = 'refactoring' ;
165+ confidence = 0.65 ;
166+ signals . push ( 'Complexity decreased' ) ;
167+ signals . push ( 'Architecture violations decreased' ) ;
158168 }
159- const curr = readJson ( currentPath ) ;
160- if ( ! curr ) {
161- console . error ( `[ratchet] Current analysis not found at ${ currentPath } . Run: npm run lint:json && npm run analyze:current` ) ;
162- process . exit ( 1 ) ;
163- return ;
169+
170+ return { intent, confidence, signals } ;
171+ }
172+
173+ function runContextMode ( base , curr ) {
174+ console . log ( '\n📊 Context-Aware Telemetry' ) ;
175+ console . log ( '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' ) ;
176+ console . log ( 'Mode: Non-blocking (burn-in period)\n' ) ;
177+
178+ console . log ( 'Category Counts:' ) ;
179+ const categories = [
180+ { key : 'magicNumbers' , label : 'Magic Numbers' } ,
181+ { key : 'complexity' , label : 'Complexity' } ,
182+ { key : 'domainTerms' , label : 'Domain Terms' } ,
183+ { key : 'architecture' , label : 'Architecture' }
184+ ] ;
185+
186+ for ( const cat of categories ) {
187+ const baseVal = base [ cat . key ] ;
188+ const currVal = curr [ cat . key ] ;
189+ const delta = currVal - baseVal ;
190+ const emoji = delta > 0 ? '⚠️' : ( delta < 0 ? '✅' : '➖' ) ;
191+ const sign = delta > 0 ? '+' : '' ;
192+ console . log ( ` ${ cat . label . padEnd ( 15 ) } ${ baseVal } → ${ currVal } (${ sign } ${ delta } ) ${ emoji } ` ) ;
164193 }
165194
166- const b = summarize ( base ) ;
167- const c = summarize ( curr ) ;
168- const { deltas, effortInc } = compare ( b , c ) ;
195+ console . log ( ) ;
196+
197+ const { intent, confidence, signals } = detectIntentFromMetrics ( base , curr ) ;
198+
199+ console . log ( 'Intent Detection:' ) ;
200+ console . log ( ` Detected: ${ intent } ` ) ;
201+ console . log ( ` Confidence: ${ ( confidence * 100 ) . toFixed ( 0 ) } %` ) ;
202+ if ( signals . length > 0 ) {
203+ console . log ( ' Signals:' ) ;
204+ signals . forEach ( s => console . log ( ` • ${ s } ` ) ) ;
205+ }
206+
207+ console . log ( '\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━' ) ;
208+ console . log ( '\n✅ Telemetry complete (non-blocking)\n' ) ;
209+ }
210+
211+ function runTraditionalMode ( base , curr , args ) {
212+ const { deltas, effortInc } = compare ( base , curr ) ;
169213
170214 // Health telemetry + optional gating
171215 const cfg = loadProjectConfig ( ) ;
172216 const healthCfg = ( cfg && cfg . ratchet && cfg . ratchet . health ) || { enabled : false } ;
173- const scores = computeHealth ( c ) ;
217+ const scores = computeHealth ( curr ) ;
174218 const gateOn = String ( healthCfg . gateOn || 'overall' ) . toLowerCase ( ) ;
175219 const minOverall = Number ( healthCfg . minOverall || 70 ) ;
176220 const intent = detectIntent ( args ) ;
@@ -181,6 +225,74 @@ function main() {
181225 const gateActive = ! ! healthCfg . enabled && ! shouldBypass ( healthCfg ) ;
182226 const gateFail = gateActive && currentScore < threshold ;
183227
228+ const strategy = ( cfg && cfg . ratchet && cfg . ratchet . strategy ) || 'classic' ;
229+
230+ // Hybrid strategy: density-based for critical vs minor categories (per 1K LOC)
231+ if ( strategy === 'hybrid' ) {
232+ const hybridCfg = ( cfg && cfg . ratchet && cfg . ratchet . hybrid ) || { } ;
233+ const critical = Array . isArray ( hybridCfg . critical ) && hybridCfg . critical . length ? hybridCfg . critical : [ 'complexity' , 'architecture' ] ;
234+ const minor = Array . isArray ( hybridCfg . minor ) && hybridCfg . minor . length ? hybridCfg . minor : [ 'domainTerms' , 'magicNumbers' ] ;
235+ const tol = ( hybridCfg . tolerance ) || { } ;
236+ const critTol = Number ( tol . criticalDensity || 1.05 ) ; // allow 5% increase
237+ const minorTol = Number ( tol . minorDensity || 1.20 ) ; // allow 20% increase
238+
239+ const baseLoc = Math . max ( 1 , Number ( base . lines && base . lines . executable ) || Number ( base . lines && base . lines . physical ) || 1 ) ;
240+ const currLoc = Math . max ( 1 , Number ( curr . lines && curr . lines . executable ) || Number ( curr . lines && curr . lines . physical ) || 1 ) ;
241+ const perK = ( count , loc ) => ( count / ( loc / 1000 ) ) ;
242+
243+ let failed = false ;
244+
245+ console . log ( '\n[ratchet] Hybrid density check (per 1K LOC)' ) ;
246+ console . log ( `LOC: ${ baseLoc } → ${ currLoc } (${ currLoc - baseLoc >= 0 ? '+' : '' } ${ currLoc - baseLoc } )` ) ;
247+
248+ console . log ( '\nCritical (strict):' ) ;
249+ for ( const cat of critical ) {
250+ const b = Number ( base [ cat ] ) || 0 ;
251+ const c = Number ( curr [ cat ] ) || 0 ;
252+ const bd = perK ( b , baseLoc ) ;
253+ const cd = perK ( c , currLoc ) ;
254+ const ratio = bd === 0 ? ( cd === 0 ? 1 : Infinity ) : ( cd / bd ) ;
255+ const status = ratio > critTol ? '❌' : '✅' ;
256+ console . log ( ` ${ String ( cat ) . padEnd ( 15 ) } ${ b } → ${ c } (density: ${ bd . toFixed ( 2 ) } → ${ cd . toFixed ( 2 ) } ) ${ status } ` ) ;
257+ if ( ratio > critTol ) failed = true ;
258+ }
259+
260+ console . log ( '\nMinor (relaxed):' ) ;
261+ for ( const cat of minor ) {
262+ const b = Number ( base [ cat ] ) || 0 ;
263+ const c = Number ( curr [ cat ] ) || 0 ;
264+ const bd = perK ( b , baseLoc ) ;
265+ const cd = perK ( c , currLoc ) ;
266+ const ratio = bd === 0 ? ( cd === 0 ? 1 : Infinity ) : ( cd / bd ) ;
267+ const status = ratio > minorTol ? '⚠️' : '✅' ;
268+ console . log ( ` ${ String ( cat ) . padEnd ( 15 ) } ${ b } → ${ c } (density: ${ bd . toFixed ( 2 ) } → ${ cd . toFixed ( 2 ) } ) ${ status } ` ) ;
269+ }
270+
271+ // Health telemetry (informational)
272+ console . log ( `\n[ratchet] Health (informational): overall=${ scores . overall } structural=${ scores . structural } semantic=${ scores . semantic } ` ) ;
273+
274+ if ( gateFail ) {
275+ console . error ( `[ratchet] HEALTH-GATE FAIL: ${ failureMessage } ` ) ;
276+ console . error ( ` gateOn=${ gateOn } threshold=${ threshold } actual=${ currentScore } intent=${ intent } ` ) ;
277+ return 1 ;
278+ }
279+
280+ if ( failed ) {
281+ console . error ( '\n❌ [ratchet] FAIL: Critical violation density increased' ) ;
282+ console . error ( 'Complexity and architecture are strictly controlled.' ) ;
283+ console . error ( 'Refactor to reduce complexity or update baseline if intentional.' ) ;
284+ return 1 ;
285+ }
286+
287+ console . log ( '\n✅ [ratchet] PASS: Quality maintained or improved' ) ;
288+ return 0 ;
289+ }
290+
291+ // Classic strategy with tolerance band
292+ // Tolerance band (configurable) to avoid blocking normal fluctuation
293+ const tolCfg = ( cfg && cfg . ratchet && cfg . ratchet . tolerance ) || { } ;
294+ const totalTolerance = Number ( tolCfg . total || 0 ) ;
295+
184296 if ( deltas . length === 0 ) {
185297 console . log ( '[ratchet] OK: no increases in analyzer categories' ) ;
186298 if ( effortInc . length ) {
@@ -192,11 +304,27 @@ function main() {
192304 if ( gateFail ) {
193305 console . error ( `[ratchet] HEALTH-GATE FAIL: ${ failureMessage } ` ) ;
194306 console . error ( ` gateOn=${ gateOn } threshold=${ threshold } actual=${ currentScore } intent=${ intent } ` ) ;
195- process . exit ( 1 ) ;
196- return ;
307+ return 1 ;
197308 }
198- process . exit ( 0 ) ;
199- return ;
309+ return 0 ;
310+ }
311+
312+ // If within tolerance, allow pass with notice
313+ const totalIncrease = deltas . reduce ( ( sum , d ) => sum + ( d . current - d . base ) , 0 ) ;
314+ if ( totalIncrease <= totalTolerance ) {
315+ console . log ( `[ratchet] OK: within tolerance (+${ totalIncrease } violations, tolerance: ${ totalTolerance } )` ) ;
316+ for ( const d of deltas ) {
317+ console . log ( ` ${ d . key } : ${ d . base } → ${ d . current } (+${ d . current - d . base } )` ) ;
318+ }
319+ console . log ( '\nSmall increases accepted during active development.' ) ;
320+ // Health telemetry (informational) and optional gate still apply
321+ console . log ( `[ratchet] Health (informational): overall=${ scores . overall } structural=${ scores . structural } semantic=${ scores . semantic } ` ) ;
322+ if ( gateFail ) {
323+ console . error ( `[ratchet] HEALTH-GATE FAIL: ${ failureMessage } ` ) ;
324+ console . error ( ` gateOn=${ gateOn } threshold=${ threshold } actual=${ currentScore } intent=${ intent } ` ) ;
325+ return 1 ;
326+ }
327+ return 0 ;
200328 }
201329
202330 console . error ( '[ratchet] FAIL: new violations introduced' ) ;
@@ -214,7 +342,42 @@ function main() {
214342 console . error ( ' - compare with analysis-baseline.json' ) ;
215343 console . error ( '\nIf intentional reductions were made and counts decreased overall, refresh baseline:' ) ;
216344 console . error ( ' npm run analyze:baseline' ) ;
217- process . exit ( 1 ) ;
345+ return 1 ;
346+ }
347+
348+ function main ( ) {
349+ const args = parseArgs ( process . argv ) ;
350+ const mode = args . mode || 'traditional' ;
351+ const baselinePath = args . baseline || args . _ [ 0 ] || 'analysis-baseline.json' ;
352+ const currentPath = args . current || args . _ [ 1 ] || 'analysis-current.json' ;
353+
354+ const base = readJson ( baselinePath ) ;
355+ if ( ! base ) {
356+ console . log ( `[ratchet] Baseline not found at ${ baselinePath } .\n` +
357+ 'Run: npm run lint:json && npm run analyze:baseline\n' +
358+ 'Skipping ratchet (non-blocking)' ) ;
359+ process . exit ( 0 ) ;
360+ return ;
361+ }
362+
363+ const curr = readJson ( currentPath ) ;
364+ if ( ! curr ) {
365+ console . error ( `[ratchet] Current analysis not found at ${ currentPath } . Run: npm run lint:json && npm run analyze:current` ) ;
366+ process . exit ( 1 ) ;
367+ return ;
368+ }
369+
370+ const b = summarize ( base ) ;
371+ const c = summarize ( curr ) ;
372+
373+ if ( mode === 'context' ) {
374+ runContextMode ( b , c ) ;
375+ process . exit ( 0 ) ;
376+ return ;
377+ }
378+
379+ const exitCode = runTraditionalMode ( b , c , args ) ;
380+ process . exit ( exitCode ) ;
218381}
219382
220383main ( ) ;
0 commit comments