@poltergeist-ai/cli 0.0.0 → 0.1.5
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/dist/cli.js +494 -46
- package/dist/index.d.ts +36 -0
- package/dist/index.js +494 -46
- package/package.json +5 -3
package/dist/cli.js
CHANGED
|
@@ -233,31 +233,31 @@ var LANG_MAP = {
|
|
|
233
233
|
var JS_TS = ["typescript", "javascript"];
|
|
234
234
|
var TS_ONLY = ["typescript"];
|
|
235
235
|
var PATTERN_RULES = [
|
|
236
|
-
// Imports
|
|
237
|
-
{ category: "import_style", choice: "named_import", pattern: /^import\s+\{/, languages: JS_TS },
|
|
238
|
-
{ category: "import_style", choice: "default_import", pattern: /^import\s+[A-Za-z_$][^\s{]*\s+from/, languages: JS_TS },
|
|
239
|
-
{ category: "import_style", choice: "path_alias", pattern: /from\s+['"][@~]\//, languages: JS_TS },
|
|
240
|
-
{ category: "import_style", choice: "relative_import", pattern: /from\s+['"]\.\.?\//, languages: JS_TS },
|
|
241
|
-
// Exports
|
|
242
|
-
{ category: "export_style", choice: "named_export", pattern: /^export\s+(?:const|function|class|type|interface|enum)\s/, languages: JS_TS },
|
|
243
|
-
{ category: "export_style", choice: "default_export", pattern: /^export\s+default\b/, languages: JS_TS },
|
|
244
|
-
{ category: "export_style", choice: "re_export", pattern: /^export\s+\{[^}]*\}\s+from/, languages: JS_TS },
|
|
245
|
-
// Functions
|
|
236
|
+
// Imports — typically project-level config (path aliases, import style)
|
|
237
|
+
{ category: "import_style", choice: "named_import", pattern: /^import\s+\{/, languages: JS_TS, lintEnforceable: true },
|
|
238
|
+
{ category: "import_style", choice: "default_import", pattern: /^import\s+[A-Za-z_$][^\s{]*\s+from/, languages: JS_TS, lintEnforceable: true },
|
|
239
|
+
{ category: "import_style", choice: "path_alias", pattern: /from\s+['"][@~]\//, languages: JS_TS, lintEnforceable: true },
|
|
240
|
+
{ category: "import_style", choice: "relative_import", pattern: /from\s+['"]\.\.?\//, languages: JS_TS, lintEnforceable: true },
|
|
241
|
+
// Exports — often enforced by barrel-file lint rules
|
|
242
|
+
{ category: "export_style", choice: "named_export", pattern: /^export\s+(?:const|function|class|type|interface|enum)\s/, languages: JS_TS, lintEnforceable: true },
|
|
243
|
+
{ category: "export_style", choice: "default_export", pattern: /^export\s+default\b/, languages: JS_TS, lintEnforceable: true },
|
|
244
|
+
{ category: "export_style", choice: "re_export", pattern: /^export\s+\{[^}]*\}\s+from/, languages: JS_TS, lintEnforceable: true },
|
|
245
|
+
// Functions — genuine opinion, not typically lint-enforced
|
|
246
246
|
{ category: "function_style", choice: "arrow_function", pattern: /(?:const|let)\s+\w+\s*=\s*(?:async\s+)?\(/, languages: JS_TS },
|
|
247
247
|
{ category: "function_style", choice: "function_declaration", pattern: /^(?:export\s+)?(?:async\s+)?function\s+\w/, languages: JS_TS },
|
|
248
|
-
// Async
|
|
248
|
+
// Async — genuine architectural choice
|
|
249
249
|
{ category: "async_style", choice: "async_await", pattern: /\bawait\s/, languages: [...JS_TS, "python"] },
|
|
250
250
|
{ category: "async_style", choice: "then_chain", pattern: /\.then\s*\(/, languages: JS_TS },
|
|
251
|
-
// Control flow
|
|
251
|
+
// Control flow — genuine opinion about code structure
|
|
252
252
|
{ category: "control_flow", choice: "early_return", pattern: /^\s+if\s*\(.*\)\s*return\b/, languages: JS_TS },
|
|
253
253
|
{ category: "control_flow", choice: "guard_clause", pattern: /^\s+if\s*\(!/, languages: JS_TS },
|
|
254
|
-
// Strings
|
|
255
|
-
{ category: "string_style", choice: "template_literal", pattern: /`[^`]*\$\{/, languages: JS_TS },
|
|
256
|
-
// Modern operators
|
|
254
|
+
// Strings — often lint-enforced (prefer-template)
|
|
255
|
+
{ category: "string_style", choice: "template_literal", pattern: /`[^`]*\$\{/, languages: JS_TS, lintEnforceable: true },
|
|
256
|
+
// Modern operators — genuine preference
|
|
257
257
|
{ category: "modern_operators", choice: "optional_chaining", pattern: /\?\.\w/, languages: JS_TS },
|
|
258
258
|
{ category: "modern_operators", choice: "nullish_coalescing", pattern: /\?\?/, languages: JS_TS },
|
|
259
259
|
{ category: "modern_operators", choice: "destructuring", pattern: /(?:const|let|var)\s+[\[{]/, languages: JS_TS },
|
|
260
|
-
// TypeScript types
|
|
260
|
+
// TypeScript types — genuine opinion about modelling
|
|
261
261
|
{ category: "type_definition", choice: "interface", pattern: /^(?:export\s+)?interface\s+\w/, languages: TS_ONLY },
|
|
262
262
|
{ category: "type_definition", choice: "type_alias", pattern: /^(?:export\s+)?type\s+\w+\s*=/, languages: TS_ONLY },
|
|
263
263
|
{ category: "enum_vs_union", choice: "enum", pattern: /^(?:export\s+)?(?:const\s+)?enum\s+\w/, languages: TS_ONLY },
|
|
@@ -265,15 +265,15 @@ var PATTERN_RULES = [
|
|
|
265
265
|
{ category: "type_features", choice: "as_const", pattern: /\bas\s+const\b/, languages: TS_ONLY },
|
|
266
266
|
{ category: "type_features", choice: "generic_usage", pattern: /<[A-Z]\w*(?:,\s*[A-Z]\w*)*>/, languages: TS_ONLY },
|
|
267
267
|
{ category: "type_features", choice: "explicit_return_type", pattern: /\)\s*:\s*(?:Promise<|void|string|number|boolean|\w+\[\])/, languages: TS_ONLY },
|
|
268
|
-
// Error handling
|
|
268
|
+
// Error handling — genuine architectural choice
|
|
269
269
|
{ category: "error_handling", choice: "try_catch", pattern: /^\s*(?:try\s*\{|\}\s*catch\s*\()/ },
|
|
270
270
|
{ category: "error_handling", choice: "custom_error", pattern: /class\s+\w+Error\s+extends/ },
|
|
271
|
-
// Testing
|
|
271
|
+
// Testing — genuine opinion about test structure
|
|
272
272
|
{ category: "test_structure", choice: "describe_it", pattern: /\b(?:describe|it)\s*\(/, languages: JS_TS },
|
|
273
273
|
{ category: "test_structure", choice: "test_fn", pattern: /\btest\s*\(/, languages: JS_TS },
|
|
274
274
|
{ category: "test_assertion", choice: "expect", pattern: /\bexpect\s*\(/, languages: JS_TS },
|
|
275
275
|
{ category: "test_assertion", choice: "assert", pattern: /\bassert\.\w/, languages: [...JS_TS, "python"] },
|
|
276
|
-
// Architecture
|
|
276
|
+
// Architecture — strong personal opinion territory
|
|
277
277
|
{ category: "composition_style", choice: "inheritance", pattern: /class\s+\w+\s+extends\s/, languages: JS_TS },
|
|
278
278
|
{ category: "composition_style", choice: "composition", pattern: /\buse[A-Z]\w+\s*\(/, languages: JS_TS },
|
|
279
279
|
{ category: "architecture", choice: "factory_function", pattern: /(?:create|make|build)[A-Z]\w+\s*\(/, languages: JS_TS },
|
|
@@ -282,7 +282,7 @@ var PATTERN_RULES = [
|
|
|
282
282
|
{ category: "python_style", choice: "type_hints", pattern: /def\s+\w+\(.*:\s*\w+/, languages: ["python"] },
|
|
283
283
|
{ category: "python_style", choice: "list_comprehension", pattern: /\[.*\bfor\b.*\bin\b.*\]/, languages: ["python"] },
|
|
284
284
|
{ category: "python_style", choice: "dataclass", pattern: /@dataclass/, languages: ["python"] },
|
|
285
|
-
{ category: "python_style", choice: "f_string", pattern: /f['"].*\{/, languages: ["python"] }
|
|
285
|
+
{ category: "python_style", choice: "f_string", pattern: /f['"].*\{/, languages: ["python"], lintEnforceable: true }
|
|
286
286
|
];
|
|
287
287
|
var CATEGORY_LABELS = {
|
|
288
288
|
import_style: "Import Style",
|
|
@@ -385,17 +385,31 @@ function extractCodeStyleFromDiff(diffOutput) {
|
|
|
385
385
|
}
|
|
386
386
|
function summariseCodeStyle(signals) {
|
|
387
387
|
const observations = [];
|
|
388
|
+
const categoryLintStatus = /* @__PURE__ */ new Map();
|
|
389
|
+
for (const rule of PATTERN_RULES) {
|
|
390
|
+
const current = categoryLintStatus.get(rule.category);
|
|
391
|
+
if (current === void 0) {
|
|
392
|
+
categoryLintStatus.set(rule.category, rule.lintEnforceable === true);
|
|
393
|
+
} else {
|
|
394
|
+
categoryLintStatus.set(
|
|
395
|
+
rule.category,
|
|
396
|
+
current && rule.lintEnforceable === true
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
388
400
|
for (const [category, choices] of Object.entries(signals.counters)) {
|
|
389
401
|
const entries = Object.entries(choices).sort((a, b) => b[1] - a[1]);
|
|
390
402
|
const total = entries.reduce((sum, [, c]) => sum + c, 0);
|
|
391
403
|
if (total < 3) continue;
|
|
392
404
|
const [topChoice, topCount] = entries[0];
|
|
405
|
+
const isLintEnforceable = categoryLintStatus.get(category) ?? false;
|
|
393
406
|
if (entries.length === 1) {
|
|
394
407
|
if (topCount >= 5) {
|
|
395
408
|
observations.push({
|
|
396
409
|
category: CATEGORY_LABELS[category] ?? category,
|
|
397
410
|
observation: `Frequently uses ${CHOICE_LABELS[topChoice] ?? topChoice} (${topCount} occurrences)`,
|
|
398
|
-
confidence: topCount >= 15 ? "strong" : "moderate"
|
|
411
|
+
confidence: topCount >= 15 ? "strong" : "moderate",
|
|
412
|
+
lintEnforceable: isLintEnforceable
|
|
399
413
|
});
|
|
400
414
|
}
|
|
401
415
|
continue;
|
|
@@ -409,7 +423,8 @@ function summariseCodeStyle(signals) {
|
|
|
409
423
|
observations.push({
|
|
410
424
|
category: CATEGORY_LABELS[category] ?? category,
|
|
411
425
|
observation: `Prefers ${topLabel} over ${runnerLabel} (${pct}% of ${total})`,
|
|
412
|
-
confidence
|
|
426
|
+
confidence,
|
|
427
|
+
lintEnforceable: isLintEnforceable
|
|
413
428
|
});
|
|
414
429
|
}
|
|
415
430
|
observations.sort((a, b) => {
|
|
@@ -427,6 +442,347 @@ function summariseCodeStyle(signals) {
|
|
|
427
442
|
import { readFileSync } from "fs";
|
|
428
443
|
|
|
429
444
|
// src/extractors/review-common.ts
|
|
445
|
+
var THEME_DEFS = [
|
|
446
|
+
{
|
|
447
|
+
theme: "decomposition",
|
|
448
|
+
label: "Decomposition / single responsibility",
|
|
449
|
+
patterns: [
|
|
450
|
+
/\bextract\b.*\b(?:into|to|out)\b/,
|
|
451
|
+
/\bsplit\b.*\b(?:into|up|out)\b/,
|
|
452
|
+
/\bseparate\b/,
|
|
453
|
+
/\bsingle.?responsib/,
|
|
454
|
+
/\bdoing\s+(?:too\s+)?(?:much|many|multiple)\b/,
|
|
455
|
+
/\btoo\s+(?:big|large|long|complex)\b/,
|
|
456
|
+
/\bbreak\b.*\b(?:into|up|out|down)\b/,
|
|
457
|
+
/\bpull\b.*\bout\b/
|
|
458
|
+
]
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
theme: "naming",
|
|
462
|
+
label: "Naming clarity",
|
|
463
|
+
patterns: [
|
|
464
|
+
/\brename\b/,
|
|
465
|
+
/\bnaming\b/,
|
|
466
|
+
/\bname\b.*\b(?:unclear|confusing|misleading|vague|generic|better)\b/,
|
|
467
|
+
/\bwhat\s+does\s+\w+\s+mean\b/,
|
|
468
|
+
/\bmore\s+descriptive\b/
|
|
469
|
+
]
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
theme: "error_handling",
|
|
473
|
+
label: "Error handling / edge cases",
|
|
474
|
+
patterns: [
|
|
475
|
+
/\berror\s+handl/,
|
|
476
|
+
/\bwhat\s+(?:if|happens\s+(?:if|when))\b/,
|
|
477
|
+
/\bedge\s+case/,
|
|
478
|
+
/\bfail(?:s|ure|ing)?\b.*\b(?:silent|graceful|handle|catch)\b/,
|
|
479
|
+
/\bunhandled\b/,
|
|
480
|
+
/\bmissing\b.*\b(?:error|catch|check|validation)\b/,
|
|
481
|
+
/\bcatch\b.*\b(?:this|here|error)\b/
|
|
482
|
+
]
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
theme: "testing",
|
|
486
|
+
label: "Testing / test coverage",
|
|
487
|
+
patterns: [
|
|
488
|
+
/\badd\b.*\btests?\b/,
|
|
489
|
+
/\btest\s+(?:for|this|that|coverage|case|missing)\b/,
|
|
490
|
+
/\buntested\b/,
|
|
491
|
+
/\bspec\b.*\b(?:for|missing|add)\b/,
|
|
492
|
+
/\bcoverage\b/,
|
|
493
|
+
/\bassert(?:ion)?\b.*\b(?:missing|add|should)\b/
|
|
494
|
+
]
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
theme: "performance",
|
|
498
|
+
label: "Performance awareness",
|
|
499
|
+
patterns: [
|
|
500
|
+
/\bperformance\b/,
|
|
501
|
+
/\bslow\b/,
|
|
502
|
+
/\boptimiz/,
|
|
503
|
+
/\bcache\b/,
|
|
504
|
+
/\bmemory\b/,
|
|
505
|
+
/\bcomplexity\b/,
|
|
506
|
+
/\bn\s*\+\s*1\b/,
|
|
507
|
+
/\bunnecessary\s+(?:re-?render|iteration|loop|allocation|copy)\b/,
|
|
508
|
+
/\bexpensive\b/
|
|
509
|
+
]
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
theme: "readability",
|
|
513
|
+
label: "Readability / clarity",
|
|
514
|
+
patterns: [
|
|
515
|
+
/\breadab/,
|
|
516
|
+
/\bclarity\b/,
|
|
517
|
+
/\bsimplif/,
|
|
518
|
+
/\bconfusing\b/,
|
|
519
|
+
/\bhard\s+to\s+(?:read|follow|understand|parse)\b/,
|
|
520
|
+
/\bcomplicated\b/,
|
|
521
|
+
/\bself[- ]?explanat/
|
|
522
|
+
]
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
theme: "type_safety",
|
|
526
|
+
label: "Type safety / strictness",
|
|
527
|
+
patterns: [
|
|
528
|
+
/\btype\s+safe/,
|
|
529
|
+
/\b(?:use|avoid|remove)\b.*\bany\b/,
|
|
530
|
+
/\bnarrow\b.*\btype/,
|
|
531
|
+
/\btype\s+assert/,
|
|
532
|
+
/\bas\s+unknown\b/,
|
|
533
|
+
/\bstrict(?:er|ly)?\s+typ/,
|
|
534
|
+
/\binfer\b.*\btype/
|
|
535
|
+
]
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
theme: "security",
|
|
539
|
+
label: "Security / input validation",
|
|
540
|
+
patterns: [
|
|
541
|
+
/\bsecur/,
|
|
542
|
+
/\bsaniti[sz]/,
|
|
543
|
+
/\bvalidat/,
|
|
544
|
+
/\binject/,
|
|
545
|
+
/\bescap(?:e|ing)\b/,
|
|
546
|
+
/\bauth(?:entication|orization)?\b/,
|
|
547
|
+
/\bxss\b/i
|
|
548
|
+
]
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
theme: "api_design",
|
|
552
|
+
label: "API / interface design",
|
|
553
|
+
patterns: [
|
|
554
|
+
/\bapi\s+(?:design|surface|contract|boundary)\b/i,
|
|
555
|
+
/\bpublic\s+(?:api|interface|surface)\b/,
|
|
556
|
+
/\bbreaking\s+change/,
|
|
557
|
+
/\bbackwards?\s+compat/,
|
|
558
|
+
/\bconsumer\b/,
|
|
559
|
+
/\bcaller\b/
|
|
560
|
+
]
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
theme: "consistency",
|
|
564
|
+
label: "Consistency with existing patterns",
|
|
565
|
+
patterns: [
|
|
566
|
+
/\bconsisten/,
|
|
567
|
+
/\belsewhere\s+(?:we|in)\b/,
|
|
568
|
+
/\bwe\s+(?:usually|always|typically|normally)\b/,
|
|
569
|
+
/\bexisting\s+(?:pattern|convention|approach|style)\b/,
|
|
570
|
+
/\bdoesn'?t\s+match\b/,
|
|
571
|
+
/\balign\b.*\bwith\b/,
|
|
572
|
+
/\bin\s+line\s+with\b/
|
|
573
|
+
]
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
theme: "documentation",
|
|
577
|
+
label: "Documentation / comments",
|
|
578
|
+
patterns: [
|
|
579
|
+
/\bdocument\b/,
|
|
580
|
+
/\bdocstring\b/,
|
|
581
|
+
/\bjsdoc\b/,
|
|
582
|
+
/\bcomment\b.*\b(?:explain|why|add|missing)\b/,
|
|
583
|
+
/\bexplain\b.*\b(?:why|what|how)\b/,
|
|
584
|
+
/\breadme\b/i
|
|
585
|
+
]
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
theme: "dependency_awareness",
|
|
589
|
+
label: "Dependency / coupling awareness",
|
|
590
|
+
patterns: [
|
|
591
|
+
/\bcoupl(?:ed|ing)\b/,
|
|
592
|
+
/\bdependen(?:cy|cies|t)\b/,
|
|
593
|
+
/\bcircular\b/,
|
|
594
|
+
/\bimport\b.*\b(?:heavy|large|unnecessary)\b/,
|
|
595
|
+
/\btight(?:ly)?\s+coupled\b/,
|
|
596
|
+
/\bdecouple\b/
|
|
597
|
+
]
|
|
598
|
+
}
|
|
599
|
+
];
|
|
600
|
+
var PRAISE_PATTERNS = [
|
|
601
|
+
/\bnice\b/,
|
|
602
|
+
/\bgreat\b/,
|
|
603
|
+
/\bgood\s+(?:call|catch|point|idea|work|job|stuff)\b/,
|
|
604
|
+
/\blove\s+(?:this|that|it|the)\b/,
|
|
605
|
+
/\bclean\b/,
|
|
606
|
+
/\belegant\b/,
|
|
607
|
+
/\blgtm\b/i,
|
|
608
|
+
/\bwell\s+done\b/,
|
|
609
|
+
/\bsolid\b/,
|
|
610
|
+
/\b(?:looks?\s+)?good\s+to\s+(?:me|go)\b/,
|
|
611
|
+
/\+1\b/,
|
|
612
|
+
/👍|🎉|✅/,
|
|
613
|
+
/\bneat\b/,
|
|
614
|
+
/\bawesome\b/
|
|
615
|
+
];
|
|
616
|
+
var EXPLANATION_PATTERNS = [
|
|
617
|
+
/\bbecause\b/,
|
|
618
|
+
/\bsince\b/,
|
|
619
|
+
/\bthe\s+reason\b/,
|
|
620
|
+
/\bso\s+that\b/,
|
|
621
|
+
/\bthis\s+(?:ensures?|prevents?|avoids?|allows?|makes?)\b/,
|
|
622
|
+
/\botherwise\b/,
|
|
623
|
+
/\bthe\s+(?:problem|issue|risk|concern)\s+(?:is|here|with)\b/
|
|
624
|
+
];
|
|
625
|
+
var SUGGESTIVE_PATTERNS = [
|
|
626
|
+
/\bconsider\b/,
|
|
627
|
+
/\bwhat\s+(?:about|if\s+we|do\s+you\s+think)\b/,
|
|
628
|
+
/\bmaybe\b/,
|
|
629
|
+
/\balternative(?:ly)?\b/,
|
|
630
|
+
/\bone\s+option\b/,
|
|
631
|
+
/\bcould\s+(?:we|you|this)\b/,
|
|
632
|
+
/\bhow\s+about\b/,
|
|
633
|
+
/\bwhat\s+(?:do\s+you|would\s+you)\s+think\b/,
|
|
634
|
+
/\bmight\s+(?:be|want)\b/,
|
|
635
|
+
/\bwould\s+(?:it\s+)?be\s+(?:better|cleaner|simpler|easier)\b/
|
|
636
|
+
];
|
|
637
|
+
var DIRECTIVE_PATTERNS = [
|
|
638
|
+
/^(?:rename|remove|delete|drop|change|move|replace|use|add|fix|update|revert|undo)\b/i,
|
|
639
|
+
/\bplease\s+(?:rename|remove|delete|change|move|replace|use|add|fix|update|revert)\b/,
|
|
640
|
+
/\bshould\s+be\b/,
|
|
641
|
+
/\bneeds?\s+to\s+be\b/,
|
|
642
|
+
/\bmust\s+be\b/,
|
|
643
|
+
/\bdon'?t\b.*\bhere\b/,
|
|
644
|
+
/\bthis\s+(?:should|must|needs?)\b/
|
|
645
|
+
];
|
|
646
|
+
function extractRecurringPhrases(comments) {
|
|
647
|
+
if (comments.length < 5) return [];
|
|
648
|
+
const phraseCounts = /* @__PURE__ */ new Map();
|
|
649
|
+
const MIN_PHRASE_LEN = 2;
|
|
650
|
+
const MAX_PHRASE_LEN = 5;
|
|
651
|
+
for (const comment of comments) {
|
|
652
|
+
const lower = comment.toLowerCase().replace(/[^\w\s]/g, " ");
|
|
653
|
+
const words = lower.split(/\s+/).filter((w) => w.length > 2);
|
|
654
|
+
const seenInComment = /* @__PURE__ */ new Set();
|
|
655
|
+
for (let start = 0; start < words.length; start++) {
|
|
656
|
+
for (let len = MIN_PHRASE_LEN; len <= MAX_PHRASE_LEN && start + len <= words.length; len++) {
|
|
657
|
+
const phrase = words.slice(start, start + len).join(" ");
|
|
658
|
+
if (!seenInComment.has(phrase)) {
|
|
659
|
+
seenInComment.add(phrase);
|
|
660
|
+
phraseCounts.set(phrase, (phraseCounts.get(phrase) ?? 0) + 1);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const minCount = Math.max(3, Math.floor(comments.length * 0.1));
|
|
666
|
+
const STOP_WORDS = /* @__PURE__ */ new Set([
|
|
667
|
+
"the",
|
|
668
|
+
"this",
|
|
669
|
+
"that",
|
|
670
|
+
"with",
|
|
671
|
+
"from",
|
|
672
|
+
"have",
|
|
673
|
+
"has",
|
|
674
|
+
"been",
|
|
675
|
+
"will",
|
|
676
|
+
"would",
|
|
677
|
+
"could",
|
|
678
|
+
"should",
|
|
679
|
+
"can",
|
|
680
|
+
"are",
|
|
681
|
+
"for",
|
|
682
|
+
"not",
|
|
683
|
+
"but",
|
|
684
|
+
"and",
|
|
685
|
+
"you",
|
|
686
|
+
"was",
|
|
687
|
+
"were",
|
|
688
|
+
"than",
|
|
689
|
+
"also",
|
|
690
|
+
"its",
|
|
691
|
+
"into",
|
|
692
|
+
"here",
|
|
693
|
+
"there",
|
|
694
|
+
"then",
|
|
695
|
+
"when",
|
|
696
|
+
"what",
|
|
697
|
+
"which",
|
|
698
|
+
"where",
|
|
699
|
+
"how"
|
|
700
|
+
]);
|
|
701
|
+
return [...phraseCounts.entries()].filter(([phrase, count]) => {
|
|
702
|
+
if (count < minCount) return false;
|
|
703
|
+
const words = phrase.split(" ");
|
|
704
|
+
return words.some((w) => !STOP_WORDS.has(w));
|
|
705
|
+
}).sort((a, b) => b[1] - a[1]).slice(0, 15).map(([phrase]) => phrase);
|
|
706
|
+
}
|
|
707
|
+
function extractRecurringQuestions(comments) {
|
|
708
|
+
const questions = comments.filter(
|
|
709
|
+
(c) => c.includes("?") && c.length > 15 && c.length < 300
|
|
710
|
+
);
|
|
711
|
+
if (questions.length < 2) return questions.slice(0, 5);
|
|
712
|
+
const normalise = (q) => q.toLowerCase().replace(/[^\w\s?]/g, "").replace(/\s+/g, " ").trim();
|
|
713
|
+
const seen = /* @__PURE__ */ new Map();
|
|
714
|
+
for (const q of questions) {
|
|
715
|
+
const norm = normalise(q);
|
|
716
|
+
let isDupe = false;
|
|
717
|
+
for (const existing of seen.keys()) {
|
|
718
|
+
if (norm === existing || norm.length > 20 && existing.startsWith(norm.slice(0, 20))) {
|
|
719
|
+
isDupe = true;
|
|
720
|
+
break;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (!isDupe) {
|
|
724
|
+
seen.set(norm, q);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return [...seen.values()].slice(0, 10);
|
|
728
|
+
}
|
|
729
|
+
function matchesAny(text, patterns) {
|
|
730
|
+
return patterns.some((p) => p.test(text));
|
|
731
|
+
}
|
|
732
|
+
function countMatching(comments, patterns) {
|
|
733
|
+
return comments.filter((c) => matchesAny(c.toLowerCase(), patterns)).length;
|
|
734
|
+
}
|
|
735
|
+
function sampleMatching(comments, patterns, max) {
|
|
736
|
+
const matches = [];
|
|
737
|
+
for (const c of comments) {
|
|
738
|
+
if (matchesAny(c.toLowerCase(), patterns)) {
|
|
739
|
+
matches.push(c.length > 200 ? c.slice(0, 200) + "..." : c);
|
|
740
|
+
if (matches.length >= max) break;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return matches;
|
|
744
|
+
}
|
|
745
|
+
function extractThemes(comments) {
|
|
746
|
+
if (comments.length === 0) return [];
|
|
747
|
+
const themes = [];
|
|
748
|
+
for (const def of THEME_DEFS) {
|
|
749
|
+
const matchingComments = [];
|
|
750
|
+
for (const comment of comments) {
|
|
751
|
+
const lower = comment.toLowerCase();
|
|
752
|
+
if (def.patterns.some((p) => p.test(lower))) {
|
|
753
|
+
matchingComments.push(comment);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (matchingComments.length < 2) continue;
|
|
757
|
+
const ratio = Math.round(matchingComments.length / comments.length * 100) / 100;
|
|
758
|
+
const snippets = matchingComments.filter((c) => c.length > 20 && c.length < 300).slice(0, 3).map((c) => c.length > 150 ? c.slice(0, 150) + "..." : c);
|
|
759
|
+
themes.push({
|
|
760
|
+
theme: def.theme,
|
|
761
|
+
label: def.label,
|
|
762
|
+
count: matchingComments.length,
|
|
763
|
+
ratio,
|
|
764
|
+
exampleSnippets: snippets
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
themes.sort((a, b) => b.count - a.count);
|
|
768
|
+
return themes;
|
|
769
|
+
}
|
|
770
|
+
function buildToneProfile(comments) {
|
|
771
|
+
if (comments.length < 5) return void 0;
|
|
772
|
+
const n = comments.length;
|
|
773
|
+
const praiseCount = countMatching(comments, PRAISE_PATTERNS);
|
|
774
|
+
const explanationCount = countMatching(comments, EXPLANATION_PATTERNS);
|
|
775
|
+
const suggestiveCount = countMatching(comments, SUGGESTIVE_PATTERNS);
|
|
776
|
+
const directiveCount = countMatching(comments, DIRECTIVE_PATTERNS);
|
|
777
|
+
return {
|
|
778
|
+
praiseRatio: Math.round(praiseCount / n * 100) / 100,
|
|
779
|
+
explanationRatio: Math.round(explanationCount / n * 100) / 100,
|
|
780
|
+
suggestiveRatio: Math.round(suggestiveCount / n * 100) / 100,
|
|
781
|
+
directiveRatio: Math.round(directiveCount / n * 100) / 100,
|
|
782
|
+
praiseExamples: sampleMatching(comments, PRAISE_PATTERNS, 3),
|
|
783
|
+
explanationExamples: sampleMatching(comments, EXPLANATION_PATTERNS, 3)
|
|
784
|
+
};
|
|
785
|
+
}
|
|
430
786
|
function summariseReview(signals) {
|
|
431
787
|
const obs = { source: signals.source };
|
|
432
788
|
const comments = signals.reviewComments;
|
|
@@ -454,6 +810,10 @@ function summariseReview(signals) {
|
|
|
454
810
|
indices.filter((i) => i >= 0 && i < n).map((i) => sorted[i])
|
|
455
811
|
)
|
|
456
812
|
];
|
|
813
|
+
obs.reviewThemes = extractThemes(comments);
|
|
814
|
+
obs.toneProfile = buildToneProfile(comments);
|
|
815
|
+
obs.recurringQuestions = extractRecurringQuestions(comments);
|
|
816
|
+
obs.recurringPhrases = extractRecurringPhrases(comments);
|
|
457
817
|
return obs;
|
|
458
818
|
}
|
|
459
819
|
|
|
@@ -755,6 +1115,16 @@ function dominantNamingStyle(style) {
|
|
|
755
1115
|
if (style.PascalCase / total > 0.7) return "Strongly prefers PascalCase";
|
|
756
1116
|
return null;
|
|
757
1117
|
}
|
|
1118
|
+
function describeTone(profile) {
|
|
1119
|
+
const traits = [];
|
|
1120
|
+
if (profile.directiveRatio > 0.3) traits.push("direct");
|
|
1121
|
+
else if (profile.suggestiveRatio > 0.3) traits.push("collaborative");
|
|
1122
|
+
if (profile.explanationRatio > 0.3) traits.push("explanatory");
|
|
1123
|
+
if (profile.praiseRatio > 0.15) traits.push("encouraging");
|
|
1124
|
+
else if (profile.praiseRatio < 0.05) traits.push("critique-focused");
|
|
1125
|
+
if (traits.length === 0) traits.push("balanced");
|
|
1126
|
+
return traits.join(", ");
|
|
1127
|
+
}
|
|
758
1128
|
function buildGhostMarkdown(input) {
|
|
759
1129
|
const { contributor, slug, gitObs, codeStyleObs, reviewObs, slackObs, docsSignals, sourcesUsed } = input;
|
|
760
1130
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
@@ -773,23 +1143,37 @@ function buildGhostMarkdown(input) {
|
|
|
773
1143
|
"",
|
|
774
1144
|
"## Review Philosophy",
|
|
775
1145
|
"",
|
|
776
|
-
"### What they care about most (ranked)"
|
|
777
|
-
"_[Fill in manually after reviewing the data below \u2014 re-order based on review comment patterns]_",
|
|
778
|
-
"1. Correctness",
|
|
779
|
-
"2. Naming",
|
|
780
|
-
"3. Component / module boundaries",
|
|
781
|
-
"4. Test coverage",
|
|
782
|
-
"5. Consistency with existing patterns",
|
|
783
|
-
"",
|
|
784
|
-
"### What they tend to ignore",
|
|
785
|
-
"_[Fill in manually]_",
|
|
786
|
-
"",
|
|
787
|
-
"### Dealbreakers",
|
|
788
|
-
"_[Fill in manually]_",
|
|
789
|
-
"",
|
|
790
|
-
"### Recurring questions / phrases"
|
|
1146
|
+
"### What they care about most (ranked)"
|
|
791
1147
|
];
|
|
792
|
-
|
|
1148
|
+
const themes = reviewObs.reviewThemes;
|
|
1149
|
+
if (themes && themes.length > 0) {
|
|
1150
|
+
lines.push(
|
|
1151
|
+
`_Inferred from ${reviewObs.totalReviewComments ?? "?"} review comments \u2014 verify and re-order as needed_`
|
|
1152
|
+
);
|
|
1153
|
+
for (let i = 0; i < Math.min(themes.length, 8); i++) {
|
|
1154
|
+
const t = themes[i];
|
|
1155
|
+
lines.push(`${i + 1}. ${t.label} _(${t.count} comments, ${Math.round(t.ratio * 100)}%)_`);
|
|
1156
|
+
}
|
|
1157
|
+
} else {
|
|
1158
|
+
lines.push(
|
|
1159
|
+
"_[Fill in manually after reviewing the data below \u2014 re-order based on review comment patterns]_",
|
|
1160
|
+
"1. Correctness",
|
|
1161
|
+
"2. Naming",
|
|
1162
|
+
"3. Component / module boundaries",
|
|
1163
|
+
"4. Test coverage",
|
|
1164
|
+
"5. Consistency with existing patterns"
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
lines.push("", "### What they tend to ignore", "_[Fill in manually]_");
|
|
1168
|
+
lines.push("", "### Dealbreakers", "_[Fill in manually]_");
|
|
1169
|
+
lines.push("", "### Recurring questions / phrases");
|
|
1170
|
+
const recurringQs = reviewObs.recurringQuestions;
|
|
1171
|
+
if (recurringQs && recurringQs.length > 0) {
|
|
1172
|
+
for (const q of recurringQs.slice(0, 8)) {
|
|
1173
|
+
const truncated = q.length > 150 ? q.slice(0, 150) + "..." : q;
|
|
1174
|
+
lines.push(`- "${truncated}"`);
|
|
1175
|
+
}
|
|
1176
|
+
} else if (reviewObs.sampleComments && reviewObs.sampleComments.length > 0) {
|
|
793
1177
|
const questions = reviewObs.sampleComments.filter((c) => c.endsWith("?"));
|
|
794
1178
|
if (questions.length > 0) {
|
|
795
1179
|
for (const q of questions.slice(0, 5)) {
|
|
@@ -802,6 +1186,19 @@ function buildGhostMarkdown(input) {
|
|
|
802
1186
|
} else {
|
|
803
1187
|
lines.push("_[Fill in from sample comments below]_");
|
|
804
1188
|
}
|
|
1189
|
+
if (themes && themes.length > 0) {
|
|
1190
|
+
lines.push("");
|
|
1191
|
+
lines.push("### Review concern examples");
|
|
1192
|
+
lines.push("_Verbatim excerpts grouped by detected theme \u2014 use these to validate the ranking above_");
|
|
1193
|
+
for (const t of themes.slice(0, 5)) {
|
|
1194
|
+
if (t.exampleSnippets.length === 0) continue;
|
|
1195
|
+
lines.push("");
|
|
1196
|
+
lines.push(`**${t.label}** (${t.count} comments):`);
|
|
1197
|
+
for (const snippet of t.exampleSnippets) {
|
|
1198
|
+
lines.push(`> ${snippet}`);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
805
1202
|
lines.push("", "---", "", "## Communication Style", "");
|
|
806
1203
|
if (reviewObs.totalReviewComments != null) {
|
|
807
1204
|
lines.push(
|
|
@@ -816,9 +1213,28 @@ function buildGhostMarkdown(input) {
|
|
|
816
1213
|
}
|
|
817
1214
|
lines.push("");
|
|
818
1215
|
}
|
|
1216
|
+
lines.push("### Tone");
|
|
1217
|
+
const tone = reviewObs.toneProfile;
|
|
1218
|
+
if (tone) {
|
|
1219
|
+
const toneDesc = describeTone(tone);
|
|
1220
|
+
lines.push(
|
|
1221
|
+
`${toneDesc} _(inferred from comment language)_`,
|
|
1222
|
+
"",
|
|
1223
|
+
`- Praise ratio: ${Math.round(tone.praiseRatio * 100)}% of comments include positive reinforcement`,
|
|
1224
|
+
`- Explanation ratio: ${Math.round(tone.explanationRatio * 100)}% of comments explain "why"`,
|
|
1225
|
+
`- Suggestive ratio: ${Math.round(tone.suggestiveRatio * 100)}% offer alternatives ("consider", "what about")`,
|
|
1226
|
+
`- Directive ratio: ${Math.round(tone.directiveRatio * 100)}% use direct imperatives ("rename this", "remove this")`
|
|
1227
|
+
);
|
|
1228
|
+
if (tone.praiseExamples.length > 0) {
|
|
1229
|
+
lines.push("", "**Praise examples:**");
|
|
1230
|
+
for (const ex of tone.praiseExamples) {
|
|
1231
|
+
lines.push(`> ${ex.length > 200 ? ex.slice(0, 200) + "..." : ex}`);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
} else {
|
|
1235
|
+
lines.push("_[Fill in manually \u2014 direct? warm? collaborative? terse?]_");
|
|
1236
|
+
}
|
|
819
1237
|
lines.push(
|
|
820
|
-
"### Tone",
|
|
821
|
-
"_[Fill in manually \u2014 direct? warm? collaborative? terse?]_",
|
|
822
1238
|
"",
|
|
823
1239
|
"### Severity prefixes they use"
|
|
824
1240
|
);
|
|
@@ -829,10 +1245,17 @@ function buildGhostMarkdown(input) {
|
|
|
829
1245
|
} else {
|
|
830
1246
|
lines.push("_[Derived from comments above \u2014 fill in which they actually use]_");
|
|
831
1247
|
}
|
|
1248
|
+
lines.push("", "### Vocabulary / phrases they use");
|
|
1249
|
+
const phrases = reviewObs.recurringPhrases;
|
|
1250
|
+
if (phrases && phrases.length > 0) {
|
|
1251
|
+
lines.push("_Phrases that appear repeatedly across their review comments:_");
|
|
1252
|
+
for (const phrase of phrases.slice(0, 10)) {
|
|
1253
|
+
lines.push(`- "${phrase}"`);
|
|
1254
|
+
}
|
|
1255
|
+
} else {
|
|
1256
|
+
lines.push("_[Fill in from sample comments below]_");
|
|
1257
|
+
}
|
|
832
1258
|
lines.push(
|
|
833
|
-
"",
|
|
834
|
-
"### Vocabulary / phrases they use",
|
|
835
|
-
"_[Fill in from sample comments below]_",
|
|
836
1259
|
"",
|
|
837
1260
|
"---",
|
|
838
1261
|
"",
|
|
@@ -873,15 +1296,18 @@ function buildGhostMarkdown(input) {
|
|
|
873
1296
|
if (gitObs.primaryExtensions || gitObs.primaryDirectories || gitObs.namingStyle || gitObs.fileTypeProfile) {
|
|
874
1297
|
lines.push("");
|
|
875
1298
|
}
|
|
876
|
-
|
|
1299
|
+
const personalObs = codeStyleObs.observations.filter((o) => !o.lintEnforceable);
|
|
1300
|
+
const lintObs = codeStyleObs.observations.filter((o) => o.lintEnforceable);
|
|
1301
|
+
if (personalObs.length > 0) {
|
|
877
1302
|
lines.push("### Detected Code Style Preferences");
|
|
1303
|
+
lines.push("_These reflect genuine contributor choices, not linter-enforced rules_");
|
|
878
1304
|
if (codeStyleObs.primaryLanguage) {
|
|
879
1305
|
lines.push(
|
|
880
1306
|
`_Primary language: ${codeStyleObs.primaryLanguage} \xB7 ${codeStyleObs.totalLinesAnalyzed} added lines analyzed_`
|
|
881
1307
|
);
|
|
882
1308
|
}
|
|
883
1309
|
lines.push("");
|
|
884
|
-
for (const obs of
|
|
1310
|
+
for (const obs of personalObs) {
|
|
885
1311
|
if (obs.confidence === "strong") {
|
|
886
1312
|
lines.push(`- **${obs.category}**: ${obs.observation}`);
|
|
887
1313
|
} else {
|
|
@@ -890,6 +1316,15 @@ function buildGhostMarkdown(input) {
|
|
|
890
1316
|
}
|
|
891
1317
|
lines.push("");
|
|
892
1318
|
}
|
|
1319
|
+
if (lintObs.length > 0) {
|
|
1320
|
+
lines.push("### Project-Level Patterns (likely lint-enforced)");
|
|
1321
|
+
lines.push("_These patterns are typically enforced by project linters/formatters \u2014 they describe the project, not the person_");
|
|
1322
|
+
lines.push("");
|
|
1323
|
+
for (const obs of lintObs) {
|
|
1324
|
+
lines.push(`- ~${obs.category}: ${obs.observation}~`);
|
|
1325
|
+
}
|
|
1326
|
+
lines.push("");
|
|
1327
|
+
}
|
|
893
1328
|
lines.push(
|
|
894
1329
|
"### Patterns they introduce / prefer"
|
|
895
1330
|
);
|
|
@@ -897,10 +1332,23 @@ function buildGhostMarkdown(input) {
|
|
|
897
1332
|
if (namingInsight) {
|
|
898
1333
|
lines.push(`- ${namingInsight}`);
|
|
899
1334
|
}
|
|
1335
|
+
if (personalObs.length > 0) {
|
|
1336
|
+
const strong = personalObs.filter((o) => o.confidence === "strong");
|
|
1337
|
+
for (const obs of strong.slice(0, 3)) {
|
|
1338
|
+
lines.push(`- ${obs.observation} _(inferred from code)_`);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
900
1341
|
lines.push(
|
|
901
1342
|
"_[Fill in manually from code inspection and review comment themes]_",
|
|
902
1343
|
"",
|
|
903
|
-
"### Patterns they push back on"
|
|
1344
|
+
"### Patterns they push back on"
|
|
1345
|
+
);
|
|
1346
|
+
if (themes && themes.length > 0) {
|
|
1347
|
+
for (const t of themes.slice(0, 3)) {
|
|
1348
|
+
lines.push(`- Violations of ${t.label.toLowerCase()} _(${t.count} review comments)_`);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
lines.push(
|
|
904
1352
|
"_[Fill in manually]_",
|
|
905
1353
|
"",
|
|
906
1354
|
"---",
|